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

イントロダクション

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の提唱する高速開発を実現するためにも、これからは少しずつベストプラクティスをまとめていけたらいいなと思います。