cakephperの日記(CakePHP, Laravel, PHP)


継続的WebセキュリティテストサービスVAddyを始めました!

2009-12-05

cakeplus.add_validation_ruleにバリデーションルール追加

cakeplusを細々と開発していますが、嬉しいことにk1LoWさんにgithub経由でpullリクエストを貰ったので、それをマージしてみました。今回追加したルールは全角のbetween、ひらがなのみ、全角のみかをチェックするの3つです。

今回はgitで一連の操作をしたので、その流れをまとめました。

CakePlusの各機能に関しては、下記の記事の中に書いてあるのでご覧下さい。

http://d.hatena.ne.jp/cakephper/20090909/1252461412

興味を持った方は、ソースコードに使い方などの説明がたくさん書いてあるので、見て頂けると嬉しいです。

http://github.com/ichikaway/cakeplus


マージの前に、とりあえず、現行のコードを初期のバージョンとしてcakeplus-0.1というタグを付けて、push。githubのall tagsという箇所からいつでも取得できるようにします。

http://github.com/ichikaway/cakeplus/tree/cakeplus-0.1

git tag cakeplus-0.1 
git push --tags

ブランチを作って、pullリクエストのコードをそこにpullする。

git branch hoge
git checkout hoge 
git pull git://github.com/k1LoW/cakeplus.git

差分は下記で見れます

git diff master hoge

テストケースとか追加してコミット

vi tests/cases/models/behaviors/add_validation_rule.test.php
git add tests/cases/models/behaviors/add_validation_rule.test.php
git commit -m "add test case"

最後にマスターに移動して、マージして、githubにpush

git checkout master
git merge hoge
git push
git branch -d hoge

git cloneして何かしらCakeplusに機能追加した人は、pullリクエスト下さい(別にメールで連絡でもいいです)。マージしますので。


追記:

今回の追加バリデーションの実装は加賀澤さんという方にして頂きました。ありがとうございます。READMEのContributor欄にお名前を加えさせて頂きました。


追記:

git pullリクエストに対応する方法が下記にまとまってます。

http://help.github.com/pull-requests/#merging_a_pull_request

自分の場合のパターン

$ git branch master-merge
$ git checkout master-merge

$ git remote add hogehoge git://github.com/hogehoge/cakeplus.git
$ git fetch hogehoge
$ git merge hogehoge/master
//ここで色々と確認、追加、修正作業

$ git co master
$ git merge master-merge
$ git push origin master

$ git branch -d maser-merge
$ git remote rm hogehoge

2009-07-27

バリデーションメッセージをDryにしつつ国際化

CakePHP1.2.3を利用しています。

CakePHPのバリデーションエラーメッセージは、各モデルに書いたりしますが、ここではgettextの__()を使った国際化の記述ができません。CakeBookにそのための回避策が一応書いてありました。

http://book.cakephp.org/ja/view/163/Localization-in-CakePHP

下記の記述をapp_model.phpに入れとけば、エラーメッセージ出力時に__()を付けてくれるので、言語ごとにエラーメッセージが切り替わります。

function invalidate($field, $value = true) {
	return parent::invalidate($field, __($value, true));
}

上記が一番楽なパターンではあるのですが、国際化対応する箇所をコマンド一発で抽出してくれる"cake i18n"コマンドだと、バリデーションエラーメッセージは抽出されません(エラーメッセージ自体は__()で記述されていないので)

ちなみにcake i18nの使い方は下記が参考になります。

http://cakephp.seesaa.net/article/87269708.html


cake i18nコマンドでもエラーメッセージが抽出できる方法はないかとIRCでK1LoWさんと話してて、それを実現する簡単なapp_modelを作ったので公開します。

ダウンロードはこちらから

http://cake.eizoku.com/source/validation_i18n.zip


機能は下記の通りです(最初の2つはvalidationのメッセージ出力をDRYにしてみる by Writing Some Codeなどで実現されています)

最後のエラーメッセージにフィールド名は、画面の上部にエラーメッセージをまとめて出力したい場合に、フィールド名がないと、どの項目のエラーか分からないので、そのための機能です。フィールド名も国際化して出力します。


ソースコード

基本の流れは、_getDefaultErrorMessagesI18n()メソッド内で連想配列エラーメッセージgettext形式で定義し、それをbeforeValidate()時にモデルのバリデーションエラーメッセージにセットしているだけです。

追記

この記事で説明するコードは色々と機能を付け足した後のやつで、もっとシンプルな初期段階のソースコードはここにあります。こっちの方が理解しやすいかもしれません。

<?php 

class AppModel extends Model {


	/**
	 * Concatenate a field name with each validation error message in replaceValidationErrorMessagesI18n().
	 * Field name is set with gettext __()
	 *   true: Concatenate
	 *   false: not Concatenate
	 *
	 * @var boolean
	 * @access protected
	 */
	var $_withFieldName = false;


	/**
	 * Error messages
	 *
	 * @var array
	 * @access protected
	 */
	var $_error_messages = array();


	/**
	 * Define default validation error messages
	 * $default_error_messages can include gettext __() value.
	 *
	 * @return array
	 * @access protected
	 */
	function _getDefaultErrorMessagesI18n(){
		//Write Default Error Message
		$default_error_messages = array(
			'require' 	=> 'Please be sure to input.',
			'email_invalid' => __('Invalid Email address.',true),
			'between' => __('Between %2$d and %3$d characters.',true),
		);

		return $default_error_messages;
	}


	/**
	 * Set validation error messages.
	 *
	 * To change default validation error messages,
	 *  set $add_error_message in each model.
	 *
	 * @param array $add_error_message
	 * @param boolean $all_change_flag
	 *    true: change all default validation error messages
	 *    false: merge $add_error_message with default validation error messages
	 * @access public
	 */
	function setErrorMessageI18n( $add_error_message = null, $all_change_flag=false ) {


		$default_error_messages = $this->_getDefaultErrorMessagesI18n();

		if( !empty( $add_error_message ) && is_array( $add_error_message ) ){
			if( $all_change_flag ){
				$default_error_messages = $add_error_message;
			}else{
				$default_error_messages = array_merge( $default_error_messages, $add_error_message );
			}
			$this->_error_messages = $default_error_messages;

		}elseif( empty($this->_error_messages)  ){
			$this->_error_messages = $default_error_messages;
		}


	}

	/**
	 * get validation error messages
	 *
	 * @return array
	 * @access protected
	 */
	function _getErrorMessageI18n(){
		return $this->_error_messages;
	}


	/**
	 * Replace validation error messages for i18n
	 *
	 * @access public
	 */
	function replaceValidationErrorMessagesI18n(){
		$this->setErrorMessageI18n();

		foreach( $this->validate as $fieldname => $ruleSet ){
			foreach( $ruleSet as $rule => $rule_info ){

				$rule_option = array();
				if(!empty($this->validate[$fieldname][$rule]['rule'])) {
					$rule_option = $this->validate[$fieldname][$rule]['rule'];
				}

				$error_message_list = $this->_getErrorMessageI18n();
				$error_message = ( array_key_exists($rule, $error_message_list ) ? $error_message_list[$rule] : null ) ;

				if( !empty( $error_message ) ) {
					$this->validate[$fieldname][$rule]['message'] = vsprintf($error_message, $rule_option);

				}elseif( !empty($this->validate[$fieldname][$rule]['message']) ){
					$this->validate[$fieldname][$rule]['message'] = 
						__( $this->validate[$fieldname][$rule]['message'], true);
				}


				if( $this->_withFieldName && !empty($this->validate[$fieldname][$rule]['message']) ){
					$this->validate[$fieldname][$rule]['message'] = 
						__( $fieldname ,true) . ' : ' . $this->validate[$fieldname][$rule]['message'];
					
				}
			}
		}
	}


	function beforeValidate(){
		$this->replaceValidationErrorMessagesI18n();
		return true;
	}

}

使い方 (基本編)

各モデルのバリデーション定義では、下記の規則に従って

var $validate = array(
	'項目名' => array( 
		'規則名' => array( 'rule' => array( 'バリデーション関数' ) ) 
	) 
);

今までと同じように定義します(下記サンプル)

var $validate = array(
	'email' => array(
		"email_invalid" => array(
			'rule' => VALID_EMAIL,
			'required' => true,
		),
	),
)

app_modelの下記の箇所にシステムで共通して使うエラーメッセージ(今後はデフォルトエラーメッセージと呼ぶことにします)を連想配列で記述します。配列のキーは各モデルで定義するバリデーションの規則名になります。

<?php
function _getDefaultErrorMessagesI18n(){
	//Write Default Error Message
	$default_error_messages = array(
		'require' 	=> 'Please be sure to input.',
		'email_invalid' => __('Invalid Email address.',true),
		'between' => __('Between %2$d and %3$d characters.',true),
	);
	return $default_error_messages;
}

今回の例だと、email_invalidのルールでエラーになった場合は、「Invalid Email address.」が国際化されて表示されます。betweenのように何文字以上という数値が変わるものも、上記例のようにすれば対応できます。



使い方 (各モデルごとにエラーメッセージを変えたい)

デフォルトエラーメッセージは、上記のようにすれば一元管理可能ですが、同じバリデーションルールでも、あるモデルではエラーメッセージを変えたい場合は、モデル側のファイルを下記のようにして上書き(マージ)が可能です

<?php
class User extends AppModel {
	function beforeValidate(){
		$error_messages = array(
			'email_invalid' => __('Oh, Invalid Email address!!!',true),

		);
		$this->setErrorMessageI18n($error_messages, false);
		parent::beforeValidate();
		return true;
	}
}

$this->setErrorMessageI18n()をapp_modelのbeforeValidate前に( parent::beforeValidate()前に )実行すれば、上書きできます。この場合だと、Userモデルの場合のみ、メールのエラーメッセージがapp_modelで定義したものから変わります。

この方法は、デフォルトエラーメッセージを残しつつ上書きしたいものだけマージする方法ですが、デフォルトエラーメッセージを全て使いたくない場合は、マージではなくて入れ替えが可能です。$this->setErrorMessageI18n()の第2引数にtrueを入れればそれが実現可能です。



使い方 (エラーメッセージにフィールド名を自動付与)

エラーメッセージを画面上部に一括で出したい場合などのために、エラーメッセージ毎にフィールド名の自動付与が可能です。app_modelの下記のプロパティをtrueにしてください。

var $_withFieldName = true;

下記のような画面になります。viewファイル側で__('email')とかフィールド名を定義して、poファイルを作っておけば、このエラーメッセージのフィールド名も日本語などになります(email:という箇所がメール:などに変わります)。

f:id:cakephper:20090727192847j:image


参考

今回の実装にあたり、下記を参考にさせていただきました。

validationのメッセージ出力をDRYにしてみる by Writing Some Code


CakePHPによる実践Webアプリケーション開発」の本(85ページあたり)



今後の予定

とりあえずプラグイン化しているので、プラグインとしてメンテしていく予定です。下記のgithubのmodels/behaviors/validation_error_i18n.phpがそれです。

http://github.com/ichikaway/cakeplus/tree


  • Task
    • デフォルトメッセージの充実
    • TestCaseの作成
    • bakeryへの記事投稿

もっと良いアイディアとか、使い方が分からないとかあれば、何でも良いのでコメント下さい。

2009-06-03

よく使う独自バリデーションルールをプラグインのbehaviorにまとめる

CakePHP1.2.3を利用しています。

今までは「独自のバリデーション関数を追加(日本語の文字数チェック)」で書いたように、独自バリデーションルールをapp_modelとかに書いてモデルで使いまわしてたんですが、そろそろ汎用的なものはプラグインでまとめて複数プロジェクトで統一して使えるようにしたいと思い移行しました。


プラグインのbehaviorに独自バリデーションルールをまとめて、モデルから利用するまでのTipsです。

まずは、プラグインに関してはSlywalkerさんのCakePHP勉強会@tokyo #4の発表資料に目を通しておくと理解が早くなります。

http://d.hatena.ne.jp/slywalker/20090523/1243059244


プラグインの場所は、app/plugins以下が基本ですが、複数プロジェクトで使いまわすので任意のディレクトリにした方が楽です。その方法は下記に書いてあります。

http://d.hatena.ne.jp/cakephper/20090303/1236069340

本エントリにおいて、任意のプラグインディレクトリにする方法は必須ではありません。面倒な人はapp/plugins以下に置いておけばOK.


さて、これからが移植の手順です。まずはプラグインディレクトリとBehaviorファイルを下記の構成で作ります。今回はcakeplusプラグインという名前で作りました。

plugins/cakeplus/models/behaviors/add_validation_rule.php

このbehaviorを呼び出す場合は、通常のモデルファイル(例えばapp/models/post.php)の中で

<?
class Post extends AppModel {

	var $name = 'Post';
	var $actsAs = array('Cakeplus.AddValidationRule');

  	var $validate = array(
 		'hoge' => array(
			"rule1" => array('rule' => array('maxLengthJP', 5),
 				'message' => 'hogeは5文字以内です'
 			),
 		),
 	);
}
?>

として、actsAsに配列プラグイン名.ファイル名 という規則で記載すれば、add_validation_rule.phpの中のメソッドが利用できるようになります。

そして、add_validation_rule.php内の独自バリデーションルール(ここではmaxLengthJP)を通常の独自バリデーションルールと同じように記述すれば使えます。



plugins/cakeplus/models/behaviors/add_validation_rule.phpの中は独自バリデーションルールを記載します。今回は日本語の文字数の上限チェックのルールを記載しています。

<?php
class AddValidationRuleBehavior extends ModelBehavior {
	/**
	 * マルチバイト用バリデーション 文字数上限チェック
	 */
	function maxLengthJP( &$model, $wordvalue, $length ) {
		$word = array_shift($wordvalue);
		return( mb_strlen( $word ) <= $length );
	}
}
?>

ここが注意点なのですが、pluginのbehaviorになるとmaxLengthJPメソッドの第一引数に渡される値が、通常のモデルやapp_model.phpで記述した場合と異なることです。第一引数には、モデル(コントローラ??)のオブジェクトが入ります。第2引数からPostされた項目の値が連想配列で入り、第3引数に上限値(ここではモデルから渡される数値の5)が入ります。

基本はこれだけです。


Postされたフィールドを利用したい場合、例えばemailの入力確認フィールドの内容が同じかどうかチェックする場合、

<?
class AddValidationRuleBehavior extends ModelBehavior {

	function checkCompare( &$model, $wordvalue , $suffix  ){

		$fieldname = key($wordvalue);

		return ( $model->data[$model->alias][$fieldname] === $model->data[$model->alias][ $fieldname . $suffix ] );


	}

}
?>

このように、第一引数で渡されたオブジェクトの中のdataの中に、モデル名.フィールド名の配列で値が入っているので、それを利用して比較します。



とりあえず今の段階で作ったbehaviorは下記になります。

plugins/cakeplus/models/behaviors/add_validation_rule.php


<?php

/**
 * 独自のバリデーションルールを追加するbehavior プラグイン
 * 内部文字コードはUTF-8(バリデーションで渡す文字データはUTF-8となります)
 *
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright 2009, Yasushi Ichikawa. (http://d.hatena.ne.jp/cakephper/)
 * @link          http://d.hatena.ne.jp/cakephper/
 * @package       cakeplus
 * @subpackage    cakeplus
 * @version       0.01
 * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
 *
 *
 * =====利用方法=====
 * 各モデルファイルで、下記のように使う。app_modelにactsAsで指定しても可
 *	var $actsAs = array('Cakeplus.AddValidationRule');
 *
 * 各モデルファイル内のバリデーションの書き方は下記を参考に。
 * 	var $validate = array(
 * 		'test' => array(
 *			"rule2" => array('rule' => array('maxLengthJP', 5),
 * 				'message' => '5文字以内です'
 * 			),
 *			"rule3" => array('rule' => array('minLengthJP', 2),
 * 				'message' => '2文字以上です'
 * 			),
 *			"rule4" => array('rule' => array('checkCompare', '_conf'),
 * 				'message' => '値が違います'
 * 			),
 * 			"rule5" => array('rule' => array('space_only'),
 * 				'message' => 'スペース以外も入力してください'
 * 			),
 * 			"rule6" => array('rule' => array('katakana_only'),
 *				'message' => 'カタカナのみ入力してください'
 * 			),
 * 		),
 * 	);
 *
 *
 */
class AddValidationRuleBehavior extends ModelBehavior {

    function setup(&$model, $config = array())
    {
        //$this->settings = $config;
        mb_internal_encoding("UTF-8");
    }


	/**
	 * マルチバイト用バリデーション 文字数上限チェック
	 *
	 * @param array &$model
	 * @param array $wordvalue
	 * @param int $length
	 * @return boolean
	 */
	function maxLengthJP( &$model, $wordvalue, $length ) {
		$word = array_shift($wordvalue);
		//return( mb_strlen( $word, mb_detect_encoding( $word ) ) <= $length );
		return( mb_strlen( $word ) <= $length );
	}

	/**
	 * マルチバイト用バリデーション 文字数下限チェック
	 *
	 * @param array &$model
	 * @param array $wordvalue
	 * @param int $length
	 * @return boolean
	 */
	function minLengthJP( &$model, $wordvalue, $length ) {
		$word = array_shift($wordvalue);
		return( mb_strlen( $word ) >= $length );
	}


	/**
	 * フィールド値の比較
	 * emailとemail_confフィールドを比較する場合などに利用
	 * _confは$suffixによって変更可能
	 *
	 * @param array &$model
	 * @param array $wordvalue
	 * @param string $suffix
	 * @return boolean
	 */
	function checkCompare( &$model, $wordvalue , $suffix  ){

		$fieldname = key($wordvalue);

		return ( $model->data[$model->alias][$fieldname] === $model->data[$model->alias][ $fieldname . $suffix ] );


	}



	/**
	 * 全角カタカナ以外が含まれていればエラーとするバリデーションチェック
	 *
	 *
	 * @param array &$model
	 * @param array $wordvalue
	 * @return boolean
	 */
	function katakana_only( &$model, $wordvalue){

	    $value = array_shift($wordvalue);

	    return preg_match("/^[ァ-ヶー゛゜]*$/u", $value);

	}




	/**
	 * 全角、半角スペースのみであればエラーとするバリデーションチェック
	 *
	 * @param array &$model
	 * @param array $wordvalue
	 * @return boolean
	 */
	function space_only( &$model, $wordvalue){

	    $value = array_shift($wordvalue);

	    if( mb_ereg_match("^(\s| )+$", $value) ){

		    return false;
	    }else{
	        return true;
	    }
	}


	/**
	 * only Allow 0-9, a-z , A-Z
	 *
	 * @param array ref &$model
	 * @param array $wordvalue
	 * @return boolean
	 */
	function alpha_number( &$model, $wordvalue ){
		$value = array_shift($wordvalue);
		return preg_match( "/^[a-zA-Z0-9]*$/", $value );

	}

}

?>


今後は下記URLのgit hubで更新していく予定です。

http://github.com/ichikaway/cakeplus/tree/master



追記(2009/7/6)

掲載していたソースコードが古くなったため、最新版(github版)に修正しました。

2008-09-05

独自のバリデーション関数を追加(日本語の文字数チェック)

cakePHP1.2rc2を使ってますが、標準のバリデーションでmaxlengthを使って文字数チェックをしようとすると、日本語に対応していないのでバイト数をカウントしてしまいます。(例えば、「あああ」だったら9文字とカウント(UTF8))

そこで、日本語の文字数をカウントしてバリデーションする関数を作成します。

バリデーションルールを記載しているモデルファイルに書いても良いですが、該当モデル以外でも汎用的に利用したかったため、今回はapp_model.phpに記載して各モデルのバリデーションルールから呼び出せるようにしました。


まず、app/app_model.phpを作り(cake/app_model.phpからコピーするだけ)、そこに下記の記述を追加。

class AppModel extends Model {

	//日本語の文字数チェック関数
	function maxLengthJP($wordvalue, $length) {
		//$wordvalueはキーがモデル名の連想配列のためforeachで対応
		//foreach ($wordvalue as $key => $value){
		//	return(mb_strlen($value,mb_detect_encoding($value)) <= $length);
		//}

		//上記よりも、こっちのほうがいいかな。結果は同じだけど。
		$value = array_shift( $wordvalue );
		return(mb_strlen($value,mb_detect_encoding($value)) <= $length);
	}

}

これで、実際のモデルファイルの方にバリデーションチェックを下記のように書けば、指定文字数より大きい文字数の場合にエラーにしてくれます。


var $validate = array(
	'companyname' => array(
		"rule1" => array('rule' => array('maxLengthJP','100'),
					'message' => '会社名は100文字以内です'
		),
	),
     )


ちなみに、function maxLengthJP($wordvalue, $length)の$wordvalueには、下記のような連想配列が渡されます。

Array
(
    [companyname] => ほげほげ
)

参考URL

http://cakephp.jp/modules/newbb/viewtopic.php?topic_id=1081&forum=6&post_id=2178

2008-09-04

cakePHPで複数パターンのバリデーションチェック

例えば選択されたラジオボタンによって必須項目が変わるような、あるモデルのファイル内で、複数パターンのバリデーションチェックをしたいと思い、昨日、新幹線の中で悶々としてました。

とりあえず下記のように対応してみた。

cakePHPのバージョンは、1.2RC1です。

モデルファイル


<?php
class Order extends AppModel {

	var $name = 'Order';

	var $validate = array(
		'username' => array(
			"rule1" => array('rule' => VALID_NOT_EMPTY,
							'required' => true,
							'message' => 'ユーザ名を入力してください'
			),
		),
	);

	var $validate2 = array(
		'companyname' => array(
			"rule1" => array('rule' => VALID_NOT_EMPTY,
							'required' => true,
							'message' => '会社名を入力してください'
			),
		),
	);

}
?>

上記のように、通常は$validateの方のバリデーションがコントローラから実行されます。

ラジオボタンで、例えば個人と法人を選択するようにしておいて、個人の場合は名前だけのチェック、法人の場合は会社名と名前の両方をチェックするという場合に、

法人の場合は$validateの後に、$validate2の方のバリデーションを実行するため、コントローラ側では下記のようにしました。


	//バリデーションチェック(名前のチェックのみ)
	$this->Order->set($this->data);
	$error = $this->validateErrors($this->Order);


	//法人の場合のチェック
	if($this->data['Order']['companyflag'] == 1){
		$this->Order->validate = $this->Order->validate2;
		$error2 = $this->validateErrors($this->Order);
		if(!empty($error2)){
			if(!empty($error)){
				$error = array_merge($error, $error2);
			}else{
				$error = $error2;
			}
		}

	}



	//バリデーションエラーがあれば、index画面に戻し、エラーを表示する
	if(!empty($error)){

		//バリデーションがエラーになったら、indexのhtmlを読み込む
		$this->set("errors",$error);
		$this->setAction("index");

	}



Viewでは、コントローラから渡される$errorの配列の内容をforeachでまわしてエラーを出力します。


<?php if(!empty($errors)){
			echo "<font color=red>";
			foreach ($errors as $error){
				echo h($error) . "<br/>";
	  		}
	  		echo "</font>";
	  }
?>



今回の方法は、コントローラで実行するバリデーションメソッド$this->validateErrorsが、モデル内の$validate変数を参照するという仕様で、$validate変数以外は参照しないという点から、

$this->Order->validate = $this->Order->validate2;

として、$validate変数に、$validate2の変数の内容を入れてしまうという方法で対応しました。


追記

もう少しスマートな方法が載ってた。

http://bakery.cakephp.org/articles/view/multivalidatablebehavior-using-many-validation-rulesets-per-model