Set::extract()の小洒落た使い方 case.1

たまには軽いネタを。もしかしたら連載するかも

<?php
$users = array(
	array(
		'User' => array(
			'id' => 1,
		),
	),
	array(
		'User' => array(
			'id' => 2,
		),
	),
	array(
		'User' => array(
			'id' => 3,
		),
	),
);

のとき、

<?php
$ids = Set::extract('/User/id', $users);

とすると

<?php
$ids = array(
	1,
	2,
	3,
);

となります。これは結構標準的な使い方ですね。

<?php
$data = array(
	'PreviousUser' => array(
		'id' => 1,
	),
	'User' => array(
		'id' => 2,
	),
	'NextUser' => array(
		'id' => 3,
	),
);

ここで、

<?php

array(
	'PreviousUser' => 1,
	'User' => 2,
	'NextUser' => 3,
);

というふうに取り出したいとします。

<?php
$ids = Set::extract('{.+?}.id', $data);

これで取り出せちゃいます。XPathのやり方はちょっと思いつきませんでした。誰か教えて!
なお、{}の間には正規表現が指定できます。柔軟ですね!

CakePHP新春勉強会で発表してきました

発表

CakePHP新春勉強会 東京CakePHP2.0について発表してきました。
初参加の方が多く幾分か緊張しましたが、今回は組長師匠のご指導により良いプレゼンにすることができたと思います。

発表のスライドはこちら
英語版のほうから書いたので日本語のほうはちょっと訳がよろしくないかも・・・(本末転倒

内容

まだ予定の半分強しか実装されていないにも関わらず、既に変更の量が膨れ上がっており、その全容を紹介することはできませでんした。
しかしつぶやきを見る限りいくらかは興味を持っていただいたようで幸いです。

他の方の発表

@camelmasaさんの事例発表でアマゾンのサービスをフル活用してスケーリングしていることは、今のプロジェクトの構成と比較しても非情に参考になることでした。
またLTも、開場の後ろで喋ってたときにすごく興味を惹かれるものが合って前のほうに気がついたら移動してたものがあったりなど、とても良かったです!

飲み会

パーティー

開場を提供してくださったジンガジャパン様が必死に働いているのを横目にグビグビ飲んでましたが、多少引け目を感じてしまいましたw
しかし、ネットでよく話をする人と喋れて、本当にこれは嬉しかったです。これからもよろしくお願いします :)

2次会

ちょっと記憶が曖昧(えー)なのですが、カラオケでLTは斬新でした。やればできるじゃない!(組長のLT的な意味で)

最後に

そんなこんなで記事を書くのが遅くなりましたが、記事を書くまでが勉強会らしいので、ようやく今回の勉強会は終わりです!
最後に会場を提供してくださったジンガジャパン様、スタッフの皆様、ならびに参加メンバーとUSTで見てくれた・twitterでつぶやいてくれた・福岡の皆さんなどなどに、伝えきれないほどの感謝を捧げます。
ありがとうございました。そしてお疲れ様でした!

CakePHP2.0関連のドキュメントの和訳

翻訳開始のお知らせ

本家wikiで、CakePHP2.0に関するドキュメントが整備されてきています。

CakePHP2.0 ドキュメント和訳
上記サイトで、順次訳していきます。よろしくお願いします。

なお、放置していましたが、CakePHP1.3移行ガイドに関しましては、既存の翻訳サイトではなく、Cookbookのほうをご覧になってください。

よき祝日を - クリスマスケーキをお届けします - CakePHP 2.0開発版がリリースされました(訳)

By markstory

CakePHPコアチームはCakePHP 2.0.0-devをこんなにも早くお見せできることを誇りに思います。これは2.xブランチの最初のパッケージリリースとなります。2.0.0-devリリースは後方非互換な変更と新機能が多くあります。そして、まだ商用利用できるようにはなっていません。一番大きい非互換な変更は、CakePHP 2.0.xが少なくともPHP 5.2.6を必要とするということです。 PHP4及び5.2.6未満のPHP5のサポートは打ち切られました。

新機能

以下はCakePHP2.0の新機能に関する完全なリストではありませんが、これまでのハイライトは含まれています:

  • テストフレームワークとしてPHPUnitの採用。PHPUnitユニットテストデファクトスタンダードであり、CakePHPも仲間入りになります。多くの後方互換性のある追加要素があるので、あなたのテストを(訳注:2.0用に)修正することも容易です。
  • リクエストとレスポンスのオブジェクトへの分割 - 必要とする情報にアクセスすること、テストを作成することが容易にできるようになります。
  • アソシエーションモデルのLazy loading - 関連付けられたモデルは、それが必要とされたときのみ読み込まれます。これは巨大なアプリケーションのメモリ使用量とパフォーマンスを著しく引き上げます。
  • ヘルパとコンポーネントのLazy loading - ヘルパとコンポーネントは必要なときのみ読み込み・初期化がなされ、パフォーマンスを向上させます。
  • 新しいコンソールフレームワーク - コンソールの枠組みは、コンソールがより柔軟に、より強力に、よりカラフル(訳注:!!)に作り直されました。
  • 例外とエラー - CakePHPは例外(Exception)を内部で使用することによって、開発者がアプリケーションでエラーと例外を、高い拡張性と設定の可変性をもって扱えるようにします。

wikiページの新機能*1でより多くのCakePHP 2.0の全ての機能を見ることができます。前述の通り、多くの後方非互換な変更がCakePHP2.0にはあります。コアチームは2.0移行ガイド*2にそれら全てを鋭意ドキュメント化しています。

もし開発中のCakePHP 2.0の助力に興味がありましたら、いくつもの方法があります。コードを実行して、見つけた問題のチケットのオープン・テスト+パッチの提供をすることができます。パッチとテストケースの作成によって、既に存在する手を付けられてないチケット*3のクローズを手伝うことができます。また、多くの仕様に関する意見や新機能について、lighthouse(訳注:CakePHPのwikiのホスト)でチケットに情報や意見を与えることができます。

この素敵な1年における、CakePHPコミュニティの全ての人に感謝します。振り返ってみると、2010年は印象的な年でした。CakePHP1.3.0のリリースを迎え、計10のリリースが1年のうちにありました。素晴らしいカンファレンスがシカゴであり、数千のドキュメントが編集され、日本コミュニティでのアドベントカレンダー*4を含む数え切れないブログ記事がありました。これら全ての素晴らしいものに加え、私達はbakeryの再リリースとhttp://tv.cakephp.org/の立ち上げもすることができました。今年は、CakePHPのもつ素晴らしいコミュニティがなければ大きな年とはならなかったことでしょう。今年を素晴らしい年にしてくれた全ての人に感謝を捧げます。


 -- 訳ここまで

後書き

Bakeryのリリース記事の訳です。
http://bakery.cakephp.org/articles/markstory/2010/12/26/happy_holiday_yule_cake_-_cakephp_2_0-dev_released

CakePHP2.0開発版が良いペースで進んでいてとても良かったです。テンション上がってきた

ので、年末年始にwikiの翻訳をします。またここで告知します。よろしくお願いします。

Re: 中間テーブルのcreated、modifiedについて

イントロダクション

CakePHPのhasAndBelongsToMany(以下habtmと呼ぶ)機構は、期待する振る舞いをしないことがあります。
CakePHPはそのマニュアルで、habtmが複雑になったときについてこう言及しています:
http://book.cakephp.org/ja/view/1034/Saving-Related-Model-Data-HABTM

また、join table (結合の情報を記述するテーブル) に (関係の作成時刻や、メタデータなどの) フィールドを付け加えたい場合にも言及しておきましょう。HABTM の join table に付け加える事もできますが、もっと簡単な方法もあります。
2つのモデルの HABTM 関係は、本当は、hasMany と belongTo による3つのモデルの関係を省略して表したものです。

このように、hasManyとbelongsToに分解することを推奨されます。
しかし、モデルとフォームが密接に関わりあってるため、これをすると、habtmの為に用意された機構が使えなくなります

  • data[HabtmModel][HabtmModel]という構造
  • FormHelper::input('HabtmModel'); といった書式
  • 自動的なリンクテーブルの更新(新しい関係性のみが保存される)

これらにより、分解したはいいがどうも使いづらい、どうすればいいんだ、という意見が沸いてくるのは当然です。
しかし、分解したからといって、アプリケーション側でその機構を作れないわけではありません

本来のhabtmと同じような振る舞い

<?php
class UsersController extends AppController {
	public function add(){
		if (!empty($this->data)) {
			if (is_array($groups = $this->data['GroupsUser']['GroupsUser'])) {
				foreach ($groups as $group_id) {
					// 整形
					$data = array(); //省略。
				}
			}
			$this->User->saveAll($data);
			// do something
		}
	}
}

FormHelperの挙動を詳しく追いかけると、望むような形式で返してくれることは、残念ながらありません。
一つの方法として、このように整形することを考え付くでしょう。
しかし、明らかにfatコントローラです。edit()の時もコピペになること間違いなしです。
これらのことは、モデル側で吸収すべきです。


さて、ごちゃごちゃ言っても始まらないので、自分なりの一つの答えを提示します。もちろん、これが唯一の正解ではないということをご理解ください。
以下、
User => Team <= Group
User:

  • id
  • name

Gourp:

  • id
  • name

Tema:

  • id
  • user_id
  • group_id

とします。

コントローラは、bakeしたものから、次のコードをadd, editの末尾に付加するだけです。

<?php $this->set('groups', $this->User->Team->Group->find('list')); ?>

Groupモデル、TeamモデルもBakeしたもので良いです。

Userモデル:(基本はやはりBake)
<?php
class User extends AppModel {
	var $name = 'User';
	var $displayField = 'name';
	//The Associations below have been created with all possible keys, those that are not needed can be removed

	var $hasMany = array(
		'Team',
	);

	function afterSave($created) {
		$groups = @(array)$this->data[$this->alias]['group_id'];
		if (!empty($groups)) {
			$old = $created ? array() : $this->Team->find('list', array('fields' => array('Team.group_id')));

			$user_id = $this->id;
			foreach (array_diff($groups, $old) as $group_id) {
				$this->Team->Group->id = $group_id;
				$this->Team->create(compact('user_id', 'group_id'));

				if (!$this->Team->Group->exists() || !$this->Team->save()) {
					return false;
				}
			}
			if (!$created) {
				$this->Team->deleteAll(array('Team.group_id' => array_diff($old, $groups)));
			}
		} elseif (!$created) {
			$this->Team->deleteAll(array('Team.user_id' => $this->id));
		}
	}

	function afterFind($results, $primary = false) {
		if ($primary) {
			foreach ($results as $index => $result) {
				$results[$index] = $this->normalizeResult($result);
			}
		}
		return $results;
	}

	// edit()の為の整形
	function normalizeResult($result) {
		if (!empty($result['Team'][0]['group_id'])) {
			$result[$this->alias]['group_id'] = Set::extract('/Team/group_id', $result);
		}
		return $result;
	}
}
add.ctp(edit.ctp)
<div class="users form">
<?php echo $this->Form->create('User');?>
	<fieldset>
 		<legend><?php __('Add User'); ?></legend>
	<?php
		echo $this->Form->input('name');
		echo $this->Form->input('group_id', array('multiple' => 'multiple')); // これを付け加えただけ
	?>
	</fieldset>
<?php echo $this->Form->end(__('Submit', true));?>
</div>
<?php // アクションリストは省略 ?>

基本はafterSave()とafterFind()で対応ということです。
おっと、これを見て以下のような疑問をもったら鋭いです:

  1. 一々こういうふうに書かなきゃいけないの?だるいな・・・
  2. Groupモデルも同じようにしたいんだけど・・・
  3. afterSave()で失敗したらfalse返してるけど、意味無いよね?トランザクションどうするの?


まず1. と2. に関してです。CakePHPにはBehaviorという恐ろしく便利なものがありますよね。
あとは言わなくてもわかると思います :)
3. に関しては、return falseの代わりにExceptionを投げましょう。
PHP4の場合は、Model::onError()をうまく使えば制御はできます。

HABTM Add & Delete Behavior

http://bakery.cakephp.org/articles/bparise/2007/05/09/add-delete-habtm-behavior
以上の解説のように、本来のhabtmの挙動とはまた違った挙動にしたい(例えば、既存のものはそのままで、新しい関係を追加(or削除)だけしたい場合も、ビヘイビアにまとめるのがいいでしょう。
上記のものは分解することなく実現していますが、概念だけは押さえておきましょう。

最後に

この問題は、自分がCakePHPを使ったはじめてのアプリケーションでぶち当たったものです。
とてつもなく悪あがきをした挙句、解決のコードもまったく美しくなく、コントローラで整形を繰り返し、また、モデルのコールバックすらも膨れ上がったものになっていまいました。
修正のチケットも投げました。賛同者もいましたが、テストコードも無い、良くなかったコミットは結局マージされることなく、間もなく1.3は新機能の取り込みを止めました。
フォーラムでは、@kanonjiさんに深く質問していただきました。
この記事は、それへの回答でもありますが、過去の自分への回答でもあります。

App::import() は凄い

CakePHP Advent Calendar2010、残りちょうど10日となりました。
15日目担当のひろみです。よろしくお願いします。

いんとろだくしょん

どんなCakeアプリケーションでも使うといっても過言では無いのがApp::import()です。
直接使うことはなくても、ヘルパーやコンポーネントをコントローラで指定すると、間接的にApp::import()を使っていることになります。(もっと言えば、dispatcherを呼ぶだけでApp::import()は呼ばれますが・・・)
そんな名脇役、App::import()について、既知の事実も含めて、詳しい挙動を追いかけてみましょう。

App::import()の歴史

CakePHP1.1までは今となっては懐かしい、uses()やvendor()を使っていました。
ヘルパやコンポーネント、モデルの読み込みすらも、グローバル関数を使って読み込みをしていたのです。
ソースコメントによると、これらをまとめてAppクラスに移行したのは、CakePHP1.2.0.6001からとなっています。
それからは、(今でもほんの一部uses()などを使用している箇所がコアコードに存在してるとはいえ、)全面的にApp::import()を使うように推奨されてきました。
CakePHP1.3からは、古いグローバル関数は公式に非推奨となり、CakePHP2.0では削除されると宣言されています。(移行ガイドなど参照)

Appクラスのコードの場所

cake/libs/ ディレクトリを見ても、app.phpなんてないよ、あれ?となるかもしれません(自分はなりました)。
実はこっそりとconfigure.phpに、Configureクラスの定義と共に存在しています。
これは、ConfigureとAppの依存関係と、ConfigureからAppへ機能が遷移していったことが(多分)関係しています。

App::import()の挙動

さて、ようやく実際の挙動についてです。
以下、基本的にCakePHP1.3での解説になります。
1.2では違う挙動になることがあるかもしれませんが、ご容赦ください。

引数

App::import()は以下の最大6つの引数を取ることができます。

  • $type = null
  • $name = null
  • $parent = true
  • $search = array()
  • $file = null
  • $return = false

このうちよく使うのは$type, $name, $parentの3つぐらいでしょう。
$typeには、ライブラリのタイプを指定します。'Controller'や、'Core'、'Lib'(1.3から)などが指定できます。
これを省略して、

<?php
App::import('File');

と書くこともできます。この場合、$typeには'Core'がデフォルトとして指定されます。
また、以下のように第一引数、第三引数には連想配列による指定が可能です。

<?php
App::import(array(
	'name' => 'File',
	'return' => true,
));

App::import(
	'Lib',
	'MyPlugin.MyLibrary',
	array(
		'return' => true,
	)
);

・$parent
falseを指定すると親クラス(AppModelなど)を読み込みません。
・$search
探索するディレクトリをランタイムで指定できます。PEARなどを呼ぶ時に便利かもしれません。
・$file
$nameから自動的にファイル名を決定せず、直接指定するときに使います。
主に外部ライブラリのファイル名がcakephp標準になってないときに指定します。
$return
true/falseでなく、include()の返り値を返すかどうかを指定します。
この場合、読込先のファイルでは return ***;を指定する必要があります。(省略すると読み込みに失敗という結果になるでしょう。)

再帰的な読み込み

App::import()は再帰的にディレクトリを探索します。
この機能により、

app/
	controllers/
		admin/
			acl_controller.php
		member/
			posts/
				posts_controller.php
				post_comments_conroller.php

などといったディレクトリを切っても、正常に読み込まれます。
コンポーネントやヘルパーの分類をすることも容易ですね。


また、例外的にapp直下にあるものは再帰的に探索しません。
デフォルトでapp直下を探索するものは、

  • Controller
  • Model
  • Helper

の3種類です。
これに関して、例えば、users_controller.phpを設置すると、App::import('Controller', 'Users')で読み込むことができてしまいます
ファイル名がモデルとヘルパで衝突するなど、望くない結果になることもあるので、app直下にapp_***.php以外のファイルは極力置かないようにしましょう。

app > cake
  • AppControllerって作ってないのに継承できるよね。でも、APP/app_controller.phpを作ってもその中身継承してくれるし・・・どういうこと?
  • app/views/layouts/default.ctpにcake/libs/views/layouts/default.ctpをコピーして書き換えたら動作した。なんでこうなるん?

以上のような疑問をもった方は多数いらっしゃると思います。
これはApp::import()が探索するパスが、あらかじめデフォルトでいくつか設定されることによるマジックです。
例えば、App::controllersというプロパティがあります。

      'ROOT/app/controllers/'
      'ROOT/app/'
      'ROOT/cake/libs/controller/'

ダンプするとこのようになっています。
これらを順に探索してゆくので、結果的にappに入っているほうを優先的に読み込むことになります。
app/libsディレクトリも例外ではなく、これによってコアライブラリの代わりに任意のライブラリを読むことも出来ます。(なるべくしないほうが身のためです)。
これに関連して、view.phpをviews/以下のどこに置いてもViewクラスの代わりに読み込んでしまいます。
なので、ビューテンプレートの拡張子はphpになりえないのです。
間違ってview.ctpをview.phpと書いてしまうと、Class View not foundといってマジギレされます。注意しましょう。


また、上に挙げたパスは、App::build()で任意のパスを追加することができます。
追加されたパスは、優先順位がデフォルトのものより高いです。
つまり、既存のファイルを変更したくない場合に別ディレクトリに読み込み先を変えることもできます。

overload

PHP4対応の為、overload()をする必要がある場合、App::import()は自動的にこれをしてくれます。
include()やrequire()などで読み込むと、これがスキップされるため、App::import()を通したほうがいいという場面が存在するでしょう。

最後に

今回はプラグインについての説明は省略させていただきました。挙動はプラグインでないものとあまり変わりはしないはずです。(本当かな・・・)


App::import()は非常に強力な反面、意外な落とし道にハマったり、余計なことをしてくれることがあるかもしれません。
また、パフォーマンス的には多少重くなるでしょう。これについては、色々な意見があると思います。(cacheをmemcachedにすれば超早いよ、とか)
もちろん、App::import()は標準的な使い方でも十分に威力を発揮します。
しかし、ここまで強力な機能が備わっているのなら、簡単な使い方だけでパフォーマンスを食うとなると、もったいないですよね。
是非、その機能を理解したうえで使い倒してみましょう!楽しいですよ。


さて、明日の担当は、aerithさんです。よろしくお願いします!!

バリデーションのベストプラクティス

イントロダクション

CakePHPの使い方は多種多様で、もちろん一つのやり方が正解ということはありません。
しかし、CakePHPフレームワークであるわけで、想定された使い方以外ではその真価をなかなか発揮できません。


CakePHPにおけるモデルは、ビジネスロジックを置くレイヤとして想定されています。
そして、バリデーションを用いることによって保存のロジックを構築するということも想定されています。
これは、何故Cookbookで紹介されるコードが、ほとんどバリデーションとModel::save()の組み合わせであるかということかの答えにもなっています。

悪い例

あなたはModel::save()の代わりとして、以下のようなadd()メソッドをモデルに定義しているかもしれません:

<?php
class Post extends AppModel {
	var $validate = array(
		'user_id' => array(
			'numeric' => array(
				'rule' => array('numeric'),
			),
		),
		// ...
	);
	var $belongsTo = array('User');

	// $user_id はコントローラのアクション側で指定($this->Auth->user('id'))
	function add($user_id, $data) {
		$this->User->id = $user_id;
		if (!$user_id || !$this->User->exists()) {
			throw new NonValidUserError();
			return false;
		}
		$data[$this->alias]['user_id'] = $user_id;
		// ...
		$this->create($data);
		return $this->save();
	}
}

これは単純に、ログインしているユーザであることをロジックとして保障するためのコードです。
user_idのバリデーションはbakeしただけのコードで、あまり意味を持ちません。

さて、このコードの何が悪いかというと、

  • 更新の時のロジックでまた同じコードを書く必要が出てくる => コピペの温床
  • 例外を投げるような汚い処理が増える(Cakeの標準機能で処理しきれなくなる) => ハマりやすい
  • バリデーションメッセージ以外を考慮に入れなければならないため、コントローラもビューも汚くなる

といった点が挙げられます。

良い例

<?php
class Post extends AppModel {
	var $validate = array(
		'user_id' => array(
			'isCurrentUser' => array(
				'rule' => array('isCurrentUser'),
				'message' => 'ログインユーザではありません',
			),
		),
		// ...
	);
	var $belongsTo = array('User');

	function isCurrentUser($check) {
		$user_id = current($check);
		// AppControllerかどこかでConfigure::write('CurrentUser' = $this->Auth->User())などとする
		if($user_id  !== Configure::read('CurrentUser.id') {
			return false;
		}
		$this->User->id = $user_id
		if (!$this->User->exists()) {
			return 'サーバ内部エラーです。管理者にお問い合わせください';
		}
		return true;
	}
}
恩恵

これで、基本的にコントローラではModel::save()を呼ぶだけです。
見慣れたコードですべて記述できるという点でも十分高速開発に貢献します。
また、バリデーションメッセージは、文字列を返すことにより変更が可能なため、どのようなエラーかを制御することができます。

より複雑なケースの例

<?php
class Post extends AppModel {
	var $validate = array(
		'user_id' => array(
			'isCurrentUser' => array(
				'rule' => array('isCurrentUser'),
				'message' => 'ログインユーザではありません',
			),
			'userHasThis' => array(
				'rule' => array('userHasThis'),
				'message' => 'この写真はあなたのものではありません',
				'on' => 'update',
			),
		),
		// ...
	);
	var $belongsTo = array('User');

	function isCurrentUser($check) {
		$user_id = current($check);
		// AppControllerかどこかでConfigure::write('CurrentUser' = $this->Auth->User())などとする
		if($user_id  !== Configure::read('CurrentUser.id') {
			return false;
		}
		$this->User->id = $user_id
		if (!$this->User->exists()) {
			return 'サーバ内部エラーです。管理者にお問い合わせください';
		}
		return true;
	}
	function userHasThis($check) {
		$user_id = current($check);

		if ($this->field('user_id') !== $user_id) {
			return false;
		}
		return true;
	}
}

このようにして、更新時のみのチェックをすることもできます。
これはbakeで生成されたコードにも含まれるので、ご存知の方も多いと思います。

whitelist

Model::save()とModel::validate()には、fieldListという名のオプションがあります。
これを指定すると、任意のフィールドのみバリデーションと保存ができます。
また、デフォルトとしてModel::whitelistにも同様に指定ができます。


例えばパスワードだけ変更するときはパスワードだけのバリデーションをしたいときにこれを使います。
新たなメソッドを用意する必要はありません。
更に、前述の'on' => 'update'などと組み合わせれば、ほとんどのケースに対して対応することができます。


しかし、これだけでは対応できないケースももちろんあります。
例えば削除時や集計、特定の条件を満たすレコード全てを更新するときです。
その場合、モデルのメソッドとしてまとめることは何も問題ありません。

まとめ

  • Cakeの基本機能から逸脱しすぎることはしない
  • バリデーションをうまく使おう
  • 基本はバリデーションの'on'オプション、whitelistを使って柔軟に対応
  • 全てのメソッドが余計となるわけではない

私感ですが、CakePHPはいよいよ枯れていく時期に入ろうとしています。
ベテランからしてみればこれらのことは当然のようなことかもしれません。
しかし、海外では浸透しているベストプラクティスは、日本では情報が少ないです。これは仕方が無いことなのかもしれません。
CakePHPの提唱する高速開発を実現するためにも、これからは少しずつベストプラクティスをまとめていけたらいいなと思います。