S2Container+S2Dao.PHP5を使ってみる(その8)

さて、カートの画面が一応出来たところで、ちょっと戻って考えます。ログイン画面で、入力されたemailとpasswordの入力値の検証はどうしようか?と。入力値のvalidationにはいくつかのレベルがあると思いますが、データベースからデータを引っ張ってこないと判断のしようのない部分はserviceまで持ち越すとして、入力されて即検証できるレベルのもの、つまり入力値が数値なのか文字列なのかだとか、最大/最小、最長/最短、はたまたある正規表現にマッチするかどうか、といったことはactionでやってしまいたいところ。UI上でJavaScriptとかで検証...とかもありますがそれは置いといて、requestで受けとった値を簡単に検証してみよう、ということで、実はここでまたAOPが使えます。SessionFilterを作ったときと同じinterceptorコマンドの、今度はvalidateタイプを選んでフィルタを生成します。入力値の型、数値の最大値/最小値、文字列の最長値/最短値、正規表現へのマッチを検証できる、BasicValidatorを作ってみました。
LoginSample\app\modules\Login\interceptor
に自動生成されますので、これに実装していきます。


validation時には、後述のvalidate設定iniファイルが使用されるのですが、template名."Suffix".iniという名前のその設定ファイルの"Suffix"の部分を、getSuffix()のreturn値に設定してやります。今回はSuffixを"validation"としました。あとはvalidate()の中身を書いていくのですが、問題のある値があれば"リクエストのパラメータ名"+Errorという名前でviewにassignしておくことにしました。これで、templateで入力エラー情報を表示させることが出来ます。validate設定ファイルの内容が解析されて$ruleという連想配列に設定されているので、それを利用してvalidationを行います。return値でvalidation後の遷移先templateの指定もできます(基本的にreturn nullでいいと思いますが)。という訳で改良の余地はかなりありそうですが、ともかく下記のとおり実装しました。

  • BasicValidator.class.php
<?php
/**
 * available properties.
 *	protected $invocation;
 *	protected $request;
 *	protected $moduleName;
 *	protected $actionName;
 *	protected $action;
 *	protected $view;
 *	protected $rule;
 *	protected $controller;
 */
class BasicValidator extends S2Base_AbstractValidateFilter
{
	public function getSuffix()
	{
		return "validation";
	}

	public function validate()
	{
		if($this->rule == null)
		{
			return null;
		}

		$page = null;
		
		foreach($this->rule as $key => $val)
		{
			$paramVal = $this->request->getParam($key);

			if(isset($val['regex']) and $this->_regexValidation($val['regex'], $paramVal) == false)
			{
				$page = $val['page'];
				$this->view->assign($key . 'Error', $val['msg']);
			}
			
			if(isset($val['type']))
			{
				if($this->_typeValidation($val['type'], $paramVal) == false)
				{
					$page = $val['page'];
 					$this->view->assign($key . 'Error', $val['msg']);
				}
				if(isset($val['min']) and !isset($val['max']))
				{
					if($this->_rangeValidation($val['type'], $val['min'], null, $paramVal) == false)
					{
						$page = $val['page'];
						$this->view->assign($key . 'Error', $val['msg']);
					}
				}
				elseif(!isset($val['min']) and isset($val['max']))
				{
					if($this->_rangeValidation($val['type'], null, $val['max'], $paramVal) == false)
					{
						$page = $val['page'];
 						$this->view->assign($key . 'Error', $val['msg']);
					}					
				}
				elseif(isset($val['min']) and isset($val['max']))
				{
					if($this->_rangeValidation($val['type'], $val['min'], $val['max'], $paramVal) == false)
					{
						$page = $val['page'];
						$this->view->assign($key . 'Error', $val['msg']);
					}
				}
			}
		}
		$this->rule = null; 
		return $page;
	}
	
	private function _regexValidation($pattern = null, $subject = null)
	{
		if($pattern == null or $pattern == "")
		{
			return true;
		}
		
		if(preg_match($pattern, $subject) == false)
		{
			return false;
		}
		return true;
	}

	 private function _typeValidation($type = null, $subject = null)
	{
		if($type == null or $type == "")
		{
			return true;
		}
		
		if($type == "numeric" and is_numeric($subject) == false)
		{
			return false;
		}
		
		if($type == "string" and is_string($subject) == false)
		{
			return false;
		}
		return true;
	}

	private function _rangeValidation($type = null, $minVal = null, $maxVal = null, $subject = null)
	{ 
		if($type == "string")
		{
			if($maxVal == null and $minVal <= strlen($subject))
			{
				return true;
			}
			elseif($minVal == null and strlen($subject) <= $maxVal)
			{
				return true;
			}
			elseif($minVal <= strlen($subject) and strlen($subject) <= $maxVal)
			{  
				return true;
			}
			return false;
		}
		elseif($type == "numeric")
		{
			if($maxVal == null and $minVal <= $subject)
			{
				return true;
			}
			elseif($minVal == null and $subject <= $maxVal)
			{
				return true;
			}
			elseif($minVal <= $subject and $subject <= $maxVal)
			{
				return true;
			}
			return false;			
		}
		return true;
	}
}
?>

次に、validate設定iniファイルについてですが、
LoginSample\app\modules\Login\validate\memberLogin.validation.ini
を作成します(これは手動でディレクトリとファイルを作成します)。また、例によってモジュールの設定ファイルLogin.inc.phpでinclude_pathに\validateが追加されるようにします。


設定ファイルの内容は、リクエストパタメータの値をセクションとして、その後にvalidateフィルタのruleに格納されるべきキーと値を並べていきます。今回は、string/numericの型チェックと、numericの場合は最大値/最小値、stringの場合は最長値/最短値、そして正規表現チェックを行い、遷移先画面、エラーメッセージを設定するというようなvalidatorとしたので、下記のようになります。ちなみに、妥当なメールアドレスを確認する正規表現が分からなかったのですが、調べると滅茶苦茶長いのが出てきました。しかも、実はこれでもパーフェクトではなく、実際はメールアドレスを正規表現で完全に表現するのは不可能とのこと!まあ、ここは適当に"/[^@]+@\w+\..+/"辺りにしときましたが、チェックは緩めにしといて、登録時には実際にそのアドレスにメール送信し、受信できたかどうかを確認するのが現実的ではないかと思います。

  • MemberLogin.validation.ini
[email]
type = "string"
regex = "/[^@]+@\w+\..+/"
min = 1
max = 255
page = null
msg  = "メールアドレスを入力してください"

[password]
type = "string"
regex = ""
min = 1
max = 20
page = null
msg  = "パスワードを入力してください"

あとはMemberLoginAction.diconでaspectを追加します。フィルター用のdiconファイルは作っていないので、
sessionFilter.filterの他に

	<component name="basicValidator" class="BasicValidator">
	</component>

と追加しています。

  • MemberLoginAction.dicon
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components21.dtd">
<components namespace="memberLoginAction">
	<include path="%S2BASE_PHP5_ROOT%/app/modules/Login/dicon/MemberLoginService.dicon"/>
	<include path="%S2BASE_PHP5_ROOT%/app/modules/Login/dicon/SessionFilter.dicon"/>

	<component name="action" class="MemberLoginAction">
		<aspect>sessionFilter.filter</aspect>
		<aspect>basicValidator</aspect>
		<property name="dto">dto</property>
		<property name="service">memberLoginService.service</property>
	</component>
	<component name="dto" class="MemberLoginDto">
	</component>
	
	<component name="basicValidator" class="BasicValidator">
	</component>
</components>

以上で、memberLoginActionの前にリクエストパラメータの値がチェックされるようになりました。あとはactionとtemplateで、validatorによって設定されたエラー情報の表示を制御してやります。変更後のtemplateは下記のようになりました。テキストボックスのあとに、エラー情報である{$emailError},{$passwordError}が追加されています。入力エラーがない場合はこれらの変数はviewに設定されていない状態になりますが、その場合はtemplateの{$emailError}などの記述は無視されるだけのようなのでこれで良さそうです。

  • memberLogin.tpl
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Login</title>
</head>
<body>

<form action="d.php" method="post">
	<input type="hidden" name="mod" value="Login"/>
	<input type="hidden" name="act" value="memberLogin"/>
	メールアドレスとパスワードを入力してログインしてください
	<br/>
	e-mail
	<br/>
	<input type="text" name="email" size="30" value="{$dto->getEmail()}"/>{$emailError}
	<br/>
	password
	<br/>
	<input type="password" name="password" size="30" value="{$dto->getPassword()}"/>{$passwordError}
	<br/>
	{$dto->getMessage()}
	<br/>
	<input type="submit" name="memberLogin:viewCart" value="ログイン"/>
	<br />
	<input type="submit" name="memberLogin:memberEntry" value="新規登録"/>
</form>

</body>
</html>

最後に、このままだとログイン画面に最初に入ってきたときもvalidatorによって入力エラーが検出され、templateに出力されてしまうので、actionにちょっとだけ細工します。追加したのは下記だけです。submitボタン以外で直接この画面にやってきたときは、dtoをviewにassignする前に、それまでにviewに割り当てられた値を全てクリアしてしまいます。これで、入力エラー情報もクリアされるという訳です。まあ、実はこれは他にも良い方法があるのかも知れませんが...。

			$view->clear_all_assign();
  • MemberLoginAction.class.php
<?php
class MemberLoginAction implements S2Base_Action
{
	private $service;
	private $dto;

	public function setService(MemberLoginService $service = null){ $this->service = $service; }
	public function getService(){ return $this->service; }

	public function setDto(MemberLoginDto $dto = null){ $this->dto = $dto; }
	public function getDto(){ return $this->dto; }

	public function execute(S2Base_Request $request, S2Base_View $view)
	{
		
		if($request->getParam('memberLogin:memberEntry') != null)
		{
			return "redirect:memberEntry";
		}
		elseif($request->getParam('memberLogin:viewCart') != null)
		{
			$this->dto->setEmail($request->getParam('email'));			
			$this->dto->setPassword($request->getParam('password'));
			$this->service->authorize($this->dto);
		
			if($this->dto->getIsAuthorized() == true)
			{
				$this->dto->setMessage("");
				$_SESSION['sessionMemberId'] = $this->dto->getId();	
				$_SESSION['sessionPassword'] = $this->dto->getPassword();	
				$view->assign('dto',$this->dto);
				return "redirect:viewCart";
			}

			$this->dto->setMessage("会員データが見つかりませんでした");
			$view->assign('dto', $this->dto);
			return "memberLogin.tpl";
		}
		else
		{
			$view->clear_all_assign();
			$view->assign('dto', $this->dto);
			return "memberLogin.tpl";
		}
	}
}
?>

続く...