AuthComponentの認証に簡単に自動ログイン機能を追加するコンポーネント

あっという間に年末です。
飲み会が多すぎて体力的にヘタってきたsanojimaruです。

今日は、CakePHPのAuthComponentを使った認証システムに、簡単に自動ログイン機能が追加できるコンポーネント「AutoLoginComponent」を書いてみました。
今回のコンセプトは「極力変更を避ける」です。

まず、ユーザー情報を格納するテーブルに、カラムを1行追加します。

ALTER TABLE users ADD COLUMN passport VARCHAR(255) default null;

ここでは、AuthComponentのデフォルトである、usersテーブルにpassportというカラムを追加しています。
テーブル名やカラム名はAuthComponentの設定を読み込んで使用しますので、各々の環境に合わせてください。

次に、AuthComponentを読み込んでいるControllerの$componentsにAutoLoginComponentを追加します。

<?php
class AppController extends Controller {
	var $components = array('Auth', 'AutoLogin');
}
?>

次に、ログイン処理とログアウト処理を実行するアクションに処理を追加します。
デフォルトは、UsersController::login(),UsersController::logout()です。

<?php
	/**
	 * ログイン処理
	 */
	function login() {
		//ログイン判定
		if($this->Auth->user()) {
			//ログインに成功した場合、自動ログインを有功にする
			$this->AutoLogin->enable();

			$this->redirect($this->Auth->redirect());
		}
	}

	/**
	 * ログアウト処理
	 */
	function logout() {
		$this->AutoLogin->disable();
		$this->redirect($this->Auth->logout());
	}
?>

最後に、app/controllers/componentsに以下のファイルを設置します。

<?php
/**
 * AuthComponentに自動ログイン機能を付加するコンポーネント.
 * 
 * @author sanojimaru
 *
 */
class AutoLoginComponent extends Object {
	/**
	 * 使用するコンポーネント
	 * @var array
	 */
	var $components = array('Cookie');
	
	/**
	 * 呼び出し元コントローラーインスタンスの参照
	 * @var Object AppController
	 */
	var $controller;
	
	/**
	 * AuthComponentが認証に使用するモデルインスタンスの参照
	 * @var Object AppModel
	 */
	var $userModel;
		
	/**
	 * 自動ログインの有効期限.
	 * strtotime()関数でtime型に変換できる文字列.
	 * デフォルトは2週間('2weeks').
	 * @var string
	 */
	var $expire;
	
	/**
	 * userModelに対応するテーブルの、自動ログイン情報を保存するフィールド名.
	 * cookieに保存する際のキーにも使用する.	 
	 * デフォルトは'passport'
	 * @var string
	 */
	var $field;
	
	/**
	 * 呼び出し元コントローラーのbeforeFilter以前に実行される.
	 * 
	 * @param $controller 呼び出し元コントローラーインスタンス
	 */
	function initialize(&$controller) {
		//自動ログインの有効期限、2週間
		$this->expire = '2weeks';
		
		//自動ログインに使用するuserModelテーブルのフィールド名
		$this->field = 'passport';
	}
	
	/**
	 * コンストラクタ
	 * @param $controller 呼び出し元コントローラーインスタンス
	 */
	function startup(&$controller) {
		//コントローラーを取得
		$this->controller = & $controller;
		//AuthComponentから認証に使うモデルを取得
		$this->userModel = & $controller->{$controller->Auth->userModel};
		//AuthComponentの自動リダイレクト設定を切る
		$controller->Auth->autoRedirect = false;
		
		//ログイン処理
		$this->login();
	}
	
	/**
	 * 自動ログインを実行する.
	 */
	function login() {
		//ログイン済みなら終了
		if($this->controller->Auth->user()) {
			return;
		}
		
		//cookieから自動ログインキーを取得
		$passport = $this->Cookie->read($this->field);
		
		//自動ログインキーが存在する場合
		if($passport) {
			$passport = $passport[$this->field];
			
			//自動ログインキーからユーザー情報を取得
			$user = $this->userModel->find('first', array(
				'conditions' => array($this->userModel->name.'.'.$this->field => $passport)
			));

			//取得したユーザー情報でログイン
			if($this->controller->Auth->login($user[$this->userModel->name])) {
				//自動ログインキーを上書き
				$this->enable();
			}
		}
	}
	
	/**
	 * 自動ログインを使用不能にする
	 */
	function disable() {
		$this->Cookie->del($this->field);
		
		$this->userModel->id = $this->controller->Auth->user('id');
		$this->userModel->saveField($this->field, null);
	}

	/**
	 * 自動ログインを使用可能にする
	 */
	function enable() {
		$new_passport = String::uuid();

		$this->userModel->id = $this->controller->Auth->user('id');
		$this->userModel->saveField($this->field, $new_passport);
		
		$this->Cookie->write($this->field, array($this->field => $new_passport), true, '+'.$this->expire);
	}
}
?>

以上で追加作業は完了です。

なお、AutoLoginComponentの独自設定は以下の2つのみです。
・自動ログインの有効期限
・自動ログインキーを保存するカラム名

上記の例の通りに設定すれば特に指定は必要ありませんが、独自に設定する場合はAutoLoginComponentを読み込んでいるコントローラー(例だとAppController)のbeforeFilter()で設定してください。

<?php
class AppController extends Controller {

	var $components = array('Auth', 'AutoLogin');

	function beforeFilter() {
		parent::beforeFilter();
		
		/** AuthComponentの設定ここから */
		$this->Auth->userModel = 'Trader';
		/** AuthComponentの設定ここまで */
		
		/** AutoLoginComponentの設定 */
		//自動ログイン有効期限
		//strtotime()で変換可能な文字列
		$this->AutoLogin->expire = '1week';

		//ユーザー情報を持つテーブルの自動ログインキーのフィールド名
		$this->AutoLogin->field = 'autologin';
		/** ここまで */
	}
}
?>

以下については、AuthComponentの設定を読み込むので、通常であれば設定不要です。
・ユーザー情報を保持するモデル

今回は、極力簡単に追加できることが目的だったので、自動ログインできるのは1ヶ所(1端末)のみです。

ツッコミやアドバイスなど、どしどしコメントください。
よろしくお願いします。

BasicValidationBehaiviorを使ってCakePHPのバリデーションを簡単に記述する

がっつり寒くなってきました。

ブログのネタになりそうなプログラムを書いてるときって基本ノリノリなので、結局ブログに書くタイミングを逃してしまうさのじ。です。

http://www.exgear.jp/blog/で見つけた、BasicValidationBehaiviorが調子良くてとてもいい感じです。
http://www.exgear.jp/blog/2009/06/cakephp-behavior%E3%81%A7%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%91%A8%E3%82%8A%E3%81%AE%E5%8A%B9%E7%8E%87%E5%8C%96%E3%82%92%E5%9B%B3%E3%82%8B/

文字コード変換の箇所など、多少環境に合わせて書き換えたのが以下です。

<?php
/**
 * バリデーション機能をまとめたbihavior.
 * 以下の機能が実装済み.
 * ・短縮されたバリデーション記述
 * ・独自バリデーションメソッド
 * ・バリデーションに対応する形式への自動変換
 * 文字コードはUTF-8のみに対応.
 * 
 * @author sano
 *
 */
class BasicValidationBehavior extends ModelBehavior {
	var $loaded = false;
	var $autoConvert = true;
	var $convert = array();
	
	#########################################################################
	/**
	 * エラーメッセージ
	 */
	#########################################################################
	var $validateMessage = array(
		// 標準バリデーション
		'alphaNumeric' 	=> '半角英数字で入力してください',
		'between' 		=> '%s文字以上%2s文字以内の半角文字を入力してください',
		'blank' 		=> '空でなければなりません',
		'cc' 			=> '正しいクレジットカード番号を入力してください',
		'custom' 		=> '入力値が正しくありません',
		'date' 			=> '日付形式で入力してください',
		'decimal' 		=> '小数点第%s位までの半角数字を入力してください。',
		'email' 		=> '正しいメールアドレスを入力してください',
		'equalTo' 		=> '入力値が%sと一致しません',
		'extension' 	=> '拡張子が正しくありません',
		'ip' 			=> 'IPアドレス形式で入力してください',
		'minLength' 	=> '%sバイト文字以上で入力してください',
		'maxLength' 	=> '%sバイト文字以内で入力してください',
		'money' 		=> '入力値が正しくありません',
		'numeric' 		=> '半角数字で入力してください',
		'phone' 		=> '電話番号形式(000-0000-0000)で入力してください',
		'postal' 		=> '郵便番号形式(000-0000)で入力してください',
		'range' 		=> '%sより大きく%sより小さい半角数字を入力してください',
		'url' 			=> 'URL形式で入力してください',
		'isUnique'		=> 'この値は既に使用されています。別の値を入力してください',
		'inList'        => '入力値が正しくありません',
		'time'          => '時:分 形式で入力してください',
		
		// 拡張バリデーション
		'valid_required'	=> '必ず入力してください',
		'valid_alNum'		=> '半角英数字で入力してください',
		'valid_maxLen'		=> '%s文字以内で入力してください',
		'valid_minLen'		=> '%s文字以上で入力してください',
		'valid_equalLen'	=> '%s文字で入力してください',
		'valid_phone' 		=> '電話番号形式で入力してください',
		'valid_zip' 		=> '郵便番号形式(000-000)で入力してください',
		'valid_zen'         => '全て全角で入力してください',
		'valid_kana'		=> '全てカタカナで入力してください',
		'valid_hirakana'	=> '全てひらがなで入力してください',
		'valid_single'		=> '全て半角で入力してください',
		'valid_confirm'		=> '入力内容が一致しません',
		'valid_email' 		=> '正しいメールアドレスを入力してください',
		'valid_emailMulti' 	=> '正しいメールアドレスを入力してください',
		'valid_ymd' 		=> '正しい日付形式で入力してください',
		'valid_jis'         => '環境依存文字・旧漢字はご利用頂けません',
	);
	
	
	#########################################################################
	/**
	 * データ整形用にカラムとルールの対応を保存
	 */
	#########################################################################
	function SetConvert(&$model, $col, $rule) {
		$this->convert[][$col] = $rule;
	}
	
	#########################################################################
	/**
	 * バリデーション定義毎のデータ整形
	 */
	#########################################################################
	function convertData(&$model, $col, $rule) {
		$before = '';
		$after = '';
		if(isset($model->data[$model->name]) && isset($model->data[$model->name][$col])){
			$before = $model->data[$model->name][$col];
			$after = $model->data[$model->name][$col] = $this->_convert($before, $rule);
		}
		elseif(isset($model->data[$col])){
			$before = $model->data[$col];
			$after = $model->data[$col] = $this->_convert($model->data[$col], $rule);
		}
	}
	
	function _convert($v, $rule){
		if($v == '') {
			return $v;
		}
		switch($rule) {
			case 'alphaNumeric':
			case 'email':
			case 'date':
			case 'email':
			case 'ip':
			case 'numeric':
			case 'url':
			case 'time':
			case 'valid_single':
			case 'valid_email':
			case 'valid_emailMulti':
			case 'valid_alNum':
				// 1バイト文字
				$v = mb_convert_kana($v, 'ras', 'UTF-8');
				break;
			
			case 'valid_zen':
				// 全角文字
				$v = mb_convert_kana($v, 'ASKV', 'UTF-8');
				break;
		
			case 'valid_kana':
				// 全角カタカナ文字
				$v = mb_convert_kana($v, 'KVC', 'UTF-8');
				break;
				
			case 'valid_hirakana':
				// 全角ひらかな文字
				$v = mb_convert_kana($v, 'HVc', 'UTF-8');
				break;
			case 'valid_phone':
				$v = mb_convert_kana($v, 'ras', 'UTF-8');
				$v = str_replace(array('','',''), '-', $v);
				break;
			case 'valid_zip':
				$v = mb_convert_kana($v, 'ras', 'UTF-8');
				$v = str_replace(array('','',''), '-', $v);
				if(strlen($v) == 7 && preg_match("/^[0-9]+$/", $v)){
					$v = substr($v,0,3) . '-' . substr($v,3);
				}
				break;
			case 'valid_ymd':
				$v = mb_convert_kana($v, 'ras', 'UTF-8');
				$v = str_replace('/', '-', $v);
				break;
		}
		return $v;
	}
	
	#########################################################################
	/**
	 * 必須項目の出力文字列設定
	 */
	#########################################################################
	var $require_string = '';
	function setRequireString(&$model, $str) {
		$this->require_string = $str;
	}
	
	#########################################################################
	/**
	 * 必須項目の場合は設定文字列を返す
	 */
	#########################################################################
	function getRequireString(&$model, $col) {
		// バリデーション定義の読み込み
		if (method_exists($model, 'loadValidate') && !$this->loaded){
			$model->loadValidate();
			$this->loaded = TRUE;
		}
		if(!isset($model->validate[$col])) return '';
		if($this->_getArrayValueRecursive('required', $model->validate[$col])){
			return $this->require_string;
		}
		return '';
	}
	
	#########################################################################
	/**
	 * 配列にキーが存在していればその値を返す
	 */
	#########################################################################
	function _getArrayValueRecursive($strKey, $arrArray) {
		$ret = false;
		while ( list($key, $value) = each($arrArray)) {
			$ret = $key === $strKey ? $value : false;
			if (is_array($value) && ! $ret) {
				$ret = $this->_getArrayValueRecursive($strKey, $value);
			}
			if ($ret) break;
		}
		return $ret;
	}
	
	#########################################################################
	/**
	 * バリデーションの実行前に初期化を行う
	 */
	#########################################################################
	function beforeValidate(&$model, $options = NULL) {
		// バリデーション定義の読み込み
		if (method_exists($model, 'loadValidate') && !$this->loaded){
			$model->loadValidate();
			$this->loaded = TRUE;
		}
		
		// 整形処理実行
		if($this->autoConvert){
			foreach($this->convert as $i => $arr){
				list($col, $rule) = each($arr);
				$this->convertData($model, $col, $rule);
			}
		}
		return TRUE;
	}
	
	#########################################################################
	/**
	 * バリデーション配列を引数の共通項のみとする
	 */
	#########################################################################
	function intersectValidate(&$model, $arg) {
		if (method_exists($model, 'loadValidate')){
			$model->loadValidate();
			$this->loaded = TRUE;
		}
		if(is_scalar($arg)){
			// for 'colA,colB'
			$okVali = array_flip(explode(',', $arg));
		}else{
			if(isset($arg[$model->name])){
				// for normal $data[model][colA]="xxx"
				$okVali = $arg[$model->name];
			}else{
				$cnt = Set::countDim($arg);
				// for saveAll $data[23][colA]="xxx"
				if($cnt == 2){
					$okVali = array_shift($arg);
				}else{
					list($col1, $col2) = each($arg);
					if(is_integer($col1)){
						// for columnArray array('colA', 'colB')
						$okVali = array_flip($arg);
					}else{
						// for columnKeyArray array('colA'=>"xxx", 'colB'=>"yyy")
						$okVali = $arg;
					}
				}
			}
		}
		$model->validate = array_intersect_key($model->validate, $okVali);
	}
	
	#########################################################################
	/**
	 * バリデーションの展開
	 */
	#########################################################################
	function setValidate(&$model, $arr) {
		foreach($arr as $col => $validate){
			$validate = str_replace(" ", "", $validate);
			$validate = trim($validate, '|');
			$vali_arr = explode('|', $validate);
			// 必須項目判定
			if(in_array('required', $vali_arr)){
				$allowEmpty = FALSE;
				$required   = TRUE;
				// 必須メッセージは優先表示(最後に再設定)
				$tmp = array_flip($vali_arr);
				unset($tmp['required']);
				$vali_arr = array_flip($tmp);
				$vali_arr[] = 'required';
			}else{
				$allowEmpty = TRUE;
				$required   = FALSE;
			}
			
			foreach($vali_arr as $rule){
				$param = "";
				if (preg_match("/(.*?)\[(.*?)\]/", $rule, $match)){
					$rule   = $match[1];
					$param  = $match[2];
				}
				if (method_exists($this, 'valid_' . $rule)){
					$rule = 'valid_' . $rule;
				}
				$msg  = '';
				if(isset($this->validateMessage[$rule])){
					$msg = $this->validateMessage[$rule];
				}else{
					$msg = $rule;
				}
				if($param && strstr($msg, '%s')){
					$tmp_p = array();
					foreach(explode(',', $param) as $_p){
						$tmp_p[] = trim($_p);
					}
					$msg = vsprintf($msg, $tmp_p);
				}
				
				// 項目ラベルを表示する場合
				$label = $col;
				if(method_exists($model, 'getLabel')) { 
					if(!($label = $model->getLabel($col))){
						$label = $col;
					}
				}
				if(strstr($msg, '{%label%}')){
					$msg = str_replace('{%label%}', $label, $msg);
				}
				
				// 項目がリスト型の場合、文言を変更する
				if ($model->Behaviors->attached('List')) { 
					$list = $model->getList($col);
					if($rule == 'valid_required' && !empty($list)){
						$msg = str_replace('入力', '選択', $msg);
					}
				}
				
				$my_rule = $rule;
				if($param != ''){
					$my_rule = array($rule);
					foreach(explode(',', $param) as $p){
						$my_rule[] = trim($p);
					}
				}
				$model->validate[$col][$rule] = array(
					'rule'       => $my_rule,
					'message'    => $msg,
					'required'   => $required,
					'allowEmpty' => $allowEmpty,
				);
				// 整形セット
				$this->SetConvert($model, $col, $rule);
			}
		}
		$this->loaded = TRUE;
	}
	
	#########################################################################
	/**
	 * メッセージのカスタマイズ
	 */
	#########################################################################
	function setMessage(&$model, $col, $rule, $message) {
		if (method_exists($this, 'valid_' . $rule)){
			$rule = 'valid_' . $rule;
		}
		if(isset($model->validate[$col]) && isset($model->validate[$col][$rule])){
			$model->validate[$col][$rule]['message'] = $message;
		}
	}
	
	#########################################################################
	/**
	 * バリデーションのクリア
	 */
	#########################################################################
	function clearValidate(&$model) {
		$this->loaded = TRUE;
		$model->validate = array();
		$this->convert = array();
	}
	
	#########################################################################
	/**
	 * 必須項目チェック
	 */
	#########################################################################
	function valid_required(&$model, &$data) {
		list($k, $v) = each($data);
		
		// 配列の場合(チェックボックス用)
		if(is_array($v)){
			foreach($v as $arr_v){
				if($arr_v){
					return TRUE;
				}
			}
			return FALSE;
		}
		
		if($v === ''){
			return FALSE;
		}else{
			return TRUE;
		}
	}
	
	#########################################################################
	/*
	 * alphaNumericのvalidationが、
	 * CentOSの場合に動作しない不具合を修正したメソッド.
	 */
	#########################################################################
	function valid_alNum(&$model, &$data) {
		list($k, $v) = each($data);
		if($v === '') return TRUE;
		//$v = mb_convert_encoding($v, 'UTF-8');
		if (!preg_match('/[^\\dA-Z]/i', $v)) {
			return TRUE;
		}
		return FALSE;
	}
	
	#########################################################################
	/**
	 * 最大文字数チェック
	 */
	#########################################################################
	function valid_maxLen(&$model, &$data, $len) {
		list($k, $v) = each($data);
		if(mb_strlen($v) > $len){
			return FALSE;
		}else{
			return TRUE;
		}
	}
	
	#########################################################################
	/**
	 * 最少文字数チェック
	 */
	#########################################################################
	function valid_minLen(&$model, &$data, $len) {
		list($k, $v) = each($data);
		if(mb_strlen($v) < $len){
			return FALSE;
		}else{
			return TRUE;
		}
	}
	#########################################################################
	/**
	 * 文字数一致チェック
	 */
	#########################################################################
	function valid_equalLen(&$model, &$data, $len) {
		list($k, $v) = each($data);
		if(mb_strlen($v) != $len){
			return FALSE;
		}else{
			return TRUE;
		}
	}
	
	#########################################################################
	/**
	 * 電話番号チェック
	 */
	#########################################################################
	function valid_phone(&$model, &$data) {
		list($k, $v) = each($data);
		if($v === '') return TRUE;
		
		if (preg_match("/^\d{2,5}\-\d{1,4}\-\d{1,4}$/", $v)) {
			return TRUE;
		}else{
			return FALSE;
		}
	}
	
	#########################################################################
	/**
	 * 郵便番号チェック
	 */
	#########################################################################
	function valid_zip(&$model, &$data) {
		list($k, $v) = each($data);
		if($v === '') return TRUE;
		
		if (preg_match("/^\d{3}\-\d{4}$/", $v)) {
			return TRUE;
		}else{
			return FALSE;
		}
	}
    
	#########################################################################
	/**
	 * 全角チェック
	 */
	#########################################################################
	function valid_zen(&$model, &$data) {
		list($k, $v) = each($data);
		if($v === '') return TRUE;
		//$v = mb_convert_encoding($v, 'UTF-8');
		if (!preg_match("/(?:\xEF\xBD[\xA1-\xBF]|\xEF\xBE[\x80-\x9F])|[\x20-\x7E]/", $v)) {
			return TRUE;
		}
		return FALSE;
	}
    
	#########################################################################
	/**
	 * カタカナチェック
	 */
	#########################################################################
	function valid_kana(&$model, &$data) {
		list($k, $v) = each($data);
		if($v === '') return TRUE;
		//$v = mb_convert_encoding($v, 'UTF-8');
		if (preg_match("/^(?:\xE3\x82[\xA1-\xBF]|\xE3\x83[\x80-\xB6]|ー|[ ]|[ ])+$/", $v)) {
			return TRUE;
		}
		return FALSE;
	}
	
	#########################################################################
	/**
	 * ひらかなチェック
	 */
	#########################################################################
	function valid_hirakana(&$model, &$data) {
		list($k, $v) = each($data);
		if($v === '') return TRUE;
		//$v = mb_convert_encoding($v, 'UTF-8');
		if (preg_match("/^(?:\xE3\x81[\x81-\xBF]|\xE3\x82[\x80-\x93])+$/", $v)) {
			return TRUE;
		}
		return FALSE;
	}
	
	#########################################################################
	/**
	 * 環境依存文字・旧漢字などJISに変換できない文字チェック
	 */
	#########################################################################
	function valid_jis(&$model, &$data) {
		list($k, $v) = each($data);
		if($v === '') return TRUE;
		$myEnc = Configure::read('App.encoding');
		// 対象外
		$v = str_replace(array('', '', '', '', '¢', '£', '¬'), "", $v);
		$v2 = mb_convert_encoding($v, 'iso-2022-jp', $myEnc);
		$v2 = mb_convert_encoding($v2, $myEnc,'iso-2022-jp');
		if ($v == $v2) {
			return TRUE;
		}
		return FALSE;
	}
	
	#########################################################################
	/**
	 *	1バイト文字列チェック
	 */
	#########################################################################
	function valid_single(&$model, &$data) {
		list($k, $v) = each($data);
		if($v === '') return TRUE;
		
		if(strlen($v) != mb_strlen($v)){
			return FALSE;
		}
		return TRUE;
	}
		
	#########################################################################
	/**
	 *	確認入力用
	 */
	#########################################################################
	function valid_confirm(&$model, &$data, $col ) {
		list($k, $v) = each($data);
		if(!isset($model->data[$model->name][$col])){
			return FALSE;
		}
		if($v === $model->data[$model->name][$col]){
			return TRUE;
		}
		return FALSE;
	}
	
	#########################################################################
	/**
	 *	メールアドレス妥当性チェック
	 */
	#########################################################################
	function valid_email(&$model, &$data ) {
		list($k, $v) = each($data);
		if($v === '') return TRUE;
		
		$__pattern = '(?:[a-z0-9][-a-z0-9]*\.)*(?:[a-z0-9][-a-z0-9]{0,62})\.(?:(?:[a-z]{2}\.)?[a-z]{2,4}|museum|travel)';
		$__regex   = '/^[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+)*@' . $__pattern . '$/i';
		
		if (preg_match($__regex, $v)) {
			return true;
		} else {
			return false;
		}
	}
	
	#########################################################################
	/**
	 *	メールアドレス妥当性チェック(複数カンマ区切り)
	 */
	#########################################################################
	function valid_emailMulti(&$model, &$data ) {
		list($k, $v) = each($data);
		if($v === '') return TRUE;
		
		$mails = explode(',', $v);
		foreach($mails as $m){
			$myData = array($k=>$m);
			if(!$this->valid_email($model, $myData)){
				return FALSE;
			}
		}
		return TRUE;
	}
	
	#########################################################################
	/**
	 *	YYYY-MM-DD形式かどうか
	 */
	#########################################################################
	function valid_ymd(&$model, &$data, $col ) {
		list($k, $v) = each($data);
		if($v === '') return TRUE;
		
		$tmp = explode('-', $v);
		if(count($tmp) != 3) return false;
		$yyyy = $tmp[0];
		$mm = $tmp[1];
		$dd = $tmp[2];
		return checkdate($mm, $dd, $yyyy);
	}
}

使用するには、モデルを以下のように編集します。

<?php
class Staff extends AppModel {
	var $name = 'Staff';

	var $actsAs = 'BasicValidation';

	/**
	 * BasicValidation用のバリデーション設定を記述する.
	 * BasicValidationBehaiviorにより、$model->validate時に実行される.
	 */
	function loadValidate() {
		$this->setValidate(array(
			'name' => 'required | maxLen[50]',
			'joined_date' => 'required | date'
		));
	}
}
?>

ただ、うちの環境ではBasicValidationBehaiviorはほとんど全てのモデルで使用したいので、$actsAs = 'BasicValidation'はAppModelに記述してあります。

1つ欠点として、$model->saveAll()の際に子のバリデーションがうまく実行されないようです。
この場合、$model->loadValidate()を明示的に実行してやることで回避できました。

	/**
	 * Branchモデルとその子であるStaffモデルを追加する.
	 * Branch hasmany Staffの関係.
	 */
	function add() {
		if (!empty($this->data)) {
			
			$this->Branch->create();

			//バリデーションを明示的にセット
			$this->Branch->loadValidate();
			$this->Branch->Staff->loadValidate();
			
			if ($this->Branch->saveAll($this->data, array('validate' => 'first'))) {
				
				$this->Session->setFlash('登録完了');
				$this->redirect(array('action'=>'index'));
				
			} else {
				
				$this->Session->setFlash('登録失敗');
				
			}
		}
	}

CakePHPのバリデーションの設定が長くて嫌気が差した方は、ぜひ使って見てください。

ubuntu9.10にeclipse+pleiadesをインストール

会社のメインマシンをubuntu9.10に変えました。
理由は、MS製品のライセンスの管理やらにうんざりしたのと、本格的にruby on railsで開発をしてみようと思い立ったから。

java出身のエンジニアの方は、eclipseでの開発に慣れてるのではないでしょうか。
私もその一人で、普段使わなくても、eclipseは必ず入ってます。

ubuntu9.10になったメインマシンにもeclipse+pleiadesで環境を作ろうかと思ったのですが、小一時間躓いたのでメモ。

まず、eclipseはapt-getでインストールします。

$ sudo apt-get install eclipse

インストールしたら、pleiadesのサイト(http://mergedoc.sourceforge.jp/)からpleiades本体をダウンロードします。
最新版、安定版は好みの問題ですが、今回は安定版を選びました。
次に、ダウンロードしたファイル(pleiades_1.3.1.zip)を/usr/lib/eclipseに展開します。
最後に、/usr/lib/eclipse/eclipse.iniを編集し、最後の行に

-javaagent:/usr/lib/eclipse/plugins/jp.sourceforge.mergedoc.pleiades/pleiades.jar

を追加します。
windowsや他のlinuxと違うのは、pleiades.jarのファイルをフルパス指定するところでしょうか。
ここまで来たら、一度-cleanをつけてeclipse起動します。

$ eclipse -clean

うまく起動できたら、次回からはランチャからの起動でOKです。

ubuntu9.10でthinkpad x61のトラックポイントの感度を調整する

ubuntu9.10には標準でマウスのスピードなどが設定できるが、標準の調整枠だとあまり体感できなかったので直接設定値をいじる。

$ sudo -s
$ cd /sys/devices/platform/i8042/serio1/sys/devices/platform/i8042/serio1

まず、今の設定値を確認。

$ cat speed
98
$ cat sensitivitiy
128

多分、speedが速度、sensitivityが感度かな?
0〜255の間で調整します。

$ echo -n 150 > speed
$ echo -n 180 > sensitivity

私はかなり軽いのが好みですのでこのぐらいですが、設定は各自調整してください。

また、このままだと再起動時に無効になってしまうので、起動時に走るスクリプトを設置します。
/etc/rc.localが既に存在する場合は追記してください。
(/etc/profile.d/だとx起動前に実行されるのか?反映されなかった。)

$ sudo vim /etc/rc.local

内容は以下。設定値は各自好みの値を入れてください。

#!/bin/bash
echo -n 150 > /sys/devices/platform/i8042/serio1/speed
echo -n 180 > /sys/devices/platform/i8042/serio1/sensitivity

以上。
一応、再起動して設定が反映されるかお試しください。

ubuntu9.10にLAMPの開発環境を構築

最新リリースのubuntu9.10、かなりいい感じです。
LAMP(php5,mysql5,apache2)の開発環境を構築したのでメモ。

#apache2インストール
$ sudo apt-get install apache2
#phpインストール
$ sudo apt-get install php5 php5-cli php-mysql
$mysqlインストール
$ sudo apt-get install mysql-sever

インストールは以上。

次に、php.iniの設定を編集します。
/etc/php5/apache2/php.iniを編集します。

自動クオートを無効化

magic_quotes_gpc = Off

言語の設定(日本語、UTF-8

mbstring.language = Japanese
mbstring.internal_encoding = UTF-8

設定の確認のため、phpinfoを見てます。

#phpファイルを作成
$ sudo vim /var/www/phpinfo.php

内容は以下のように編集。

<?php phpinfo(); ?>

あとは、http://localhost/phpinfo.phpをブラウザで確認して、phpinfoが正常に表示されていれば成功です。

MySQLの設定ですが、取り急ぎ必要なのは文字コードの設定。
文字化け防止のため、デフォルトの文字コードUTF-8にし、自動文字コード変換をスキップするという設定をします。
以下の文字列を/etc/mysql/my.cnfに追加します。

default-character-set=utf8
skip-character-set-client-handshake

以上です。

ubuntu9.10でthinkpad x61のトラックポイントスクロールを使う

ubuntu9.10が正式リリースされましたね。

早速、うちの持ち運び用ノート(thinkpad X61)にインストールしました。
このノートですが、今まではネットブック的な使い方しかしてなかったのですが、会社が都内に移転したことをキッカケにカフェで仕事とかしてみたくなったので、開発もしやすい環境にしようと思ってます。
docomoのmoperaU(公衆無線LAN)に申し込んだので、今度インプレしますね。

それはそうと、X61を選んだ理由でもあるトラックポイントの真ん中ボタンで行うスクロール機能を使うために結構ハマったのでメモ。

まず、設定ファイルを作ります。

$ sudo vim /etc/hal/fdi/policy/mouse-wheel.fdi

内容は以下。

<match key="info.product" string="TPPS/2 IBM TrackPoint">
<merge key="input.x11_options.EmulateWheel" type="string">true</merge>
<merge key="input.x11_options.EmulateWheelButton" type="string">2</merge>
<merge key="input.x11_options.XAxisMapping" type="string">6 7</merge>
<merge key="input.x11_options.YAxisMapping" type="string">4 5</merge>
<merge key="input.x11_options.ZAxisMapping" type="string">4 5</merge>
<merge key="input.x11_options.Emulate3Buttons" type="string">true</merge>
</match>

これで、Xを再起動すれば使えるようになるはずですが、何度やってもダメ。
どうやら、キャッシュを削除しないと反映されないようです。

#キャッシュを削除
$ sudo rm -f /var/cache/hald/fdi-cache
#Xを再起動
$ sudo service hal restart
$ sudo service gdm restart

やっと使えるようになりました。
ちなみに、自分の環境ではデフォルトでviの挙動がおかしかったので、vimをインストールして使ってます。

それにしてもubuntu、いいですね。
leopardに似てきた気もします。

Google App Engineを使ってみた

今話題のGoogle App Engine(GAE)を使ってみました。
PythonJava両方でHello world!したぐらいなので、詳しい説明は何か作った時に。
それだけだとアレなので、拙い文章で所感と総評を書いてみました。

【所感】
・思ってたよりレスポンスが早い
サーバーが北米にある割りに、個人向けレンタルサーバーに遜色ない感じ。
日本人の方が作ったサイトをいくつか見たけど、大規模にならない限り十分なのでは。
(例→http://www.amabiki.com/

・開発環境が充実してる
Googleが出してくれたSDKが良かった。windows版を使ってみたけど、GUIでプロジェクトの作成からテストサーバー、デプロイまで面倒みてくれる。

Javaだとeclipse google pluginなるものがあって、従来のeclipseに追加するだけで簡単に環境が整う。こちらも同様に面倒を見てくれる。

Pythonの場合eclipseが使い辛いが、そもそもLLな人はIDEよりエディタ派が多いと思うから問題なさそう。また、定番フレームワークであるDjangoを使うためのヘルパーも用意されており、Python書ける人はとても入りやすい気がした。

google依存がちょっと怖い
会社として出すサービスで使うにはちょっと気が引ける。
SLAが曖昧なのも一つだけど、一番怖いのはgoogle依存になること。
突然のサービス停止とか、ふとした拍子の規約違反でアカウントが即停止になったりとか。常時稼働のWEBサービスとしては使い辛い。

【総評】
プログラマーとして、クラウドコンピューティングの先駆け的なサービスに無料で触れるのはありがたい。
当面は、一時的な高負荷が見込まれる、永続的でないサービスに対して使ってみようと思った。
年内に何か作ってみることにする。



Google App Engine
http://code.google.com/intl/ja/appengine/

全然わからない人は、
http://code.google.com/intl/ja/appengine/docs/whatisgoogleappengine.html
とか
http://codezine.jp/article/detail/3835
とか読んでみてください。