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

どうでもいいけど、firefoxで日記のフォームに直接書いててプレビューしようとしたら無反応になり、書いた内容がパーになってしまった...。結構時間掛かったのに!めげずにもう一回書きます。今後はエディタで書いてフォームに貼り付けよう...。


その5まででログイン画面が一応できましたが、そこでログインに成功するとviewCartに飛ぶようにしています。じゃあ次にviewCartを作ろうかな、と言いたいところですが、ここで先にセッションについて考えます。ログイン画面からカートに遷移する時、POSTでemailとpasswordが渡されるのですが、ログイン画面でセッションIDが振られて、それがURLで渡されていればそこから会員を特定できるようにした方が良さそうです。


セッションもデータベースで管理したいので、sessionServiceを作ります(データベースを利用する処理は迷わずserviceを生成してしまう)。また、セッション管理用のテーブルsessionsを生成します。セッションIDは32文字の文字列なので、主キーはVARCHAR(32)とします。

CREATE TABLE sessions (
       id VARCHAR(32) NOT NULL
     , member_id INTEGER
     , password VARCHAR(20)
     , data VARCHAR(255)
     , timestamp DATETIME NOT NULL
     , PRIMARY KEY (id)
);
CREATE INDEX IX_member_id ON sessions (member_id ASC);

diconファイルは、自動設定のままで大丈夫。dxoを設定してますが実際には使いません。そのままにしといて問題無いので放置。

  • SessionService.dicon
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components21.dtd">
<components namespace="sessionService">
	<include path="%S2BASE_PHP5_ROOT%/app/commons/dicon/dao.dicon"/>
		<property name="dxo">dxo</property>
		<property name="sessionDao">sessionDao</property>
	</component>
	
	<component name="dxo" class="SessionDxoImpl">
	</component>
	
	<component name="sessionDao" class="SessionDao">
		<aspect>dao.interceptor</aspect>
	</component>
	
</components>

自動生成したsessionServiceに実装していきます。まずinterfaceですが、セッションをスタートするだけなので、メソッドはstart一つだけです。

  • SessionService.class.php
<?php
interface SessionService
{	
	public function start();
}
?>

次に実装ですが、ややヘボ目な実装になってる気がしますがとりあえず...。open/closeはあまり役に立たないので単にtrueを返してます。readは、セッションIDが存在すればdataフィールドを返し、さもなくば空文字列""を返します。writeはセッション変数sessionMemberId, sessionPasswordが存在すればそれをmember_id、passwordフィールドへ設定し、セッションIDが存在すればupdate、存在しないなら新たにinsertします。destroyはセッションIDを指定して削除します。gcは、予め定数SESSION_GARBAGE_COLLECT_INTERVALで設定された日数より前のtimestampになっているセッションを削除します。SESSION_GARBAGE_COLLECT_INTERVALは
LoginSample\app\modules\Login\Login.inc.php

define('SESSION_GARBAGE_COLLECT_INTERVAL', 30);
とか書いとけばいいです。
上記関数をprivateで定義して、startメソッドはsession_set_save_handler()でそれらの関数をセッション処理に使うように指定します。あとはsession_start()ですね。read, write, destroy, gcでsessionsテーブルに読み書きするので、daoにそれ用のメソッドを書き加えます。

  • SessionServiceImpl.class.php
<?php
class SessionServiceImpl implements SessionService
{
	private $dxo;
	private $sessionDao;

	public function setDxo(SessionDxo $value = null){ $this->dxo = $value; }
	public function getDxo(){ return $this->dxo; }

	public function setSessionDao(SessionDao $value = null){ $this->sessionDao = $value; }
	public function getSessionDao(){ return $this->sessionDao; }

	public function __construct(){}
	
	public function start()
	{
		session_set_save_handler(
			array(&$this, '_session_open'),
			array(&$this, '_session_close'),
			array(&$this, '_session_read'),
			array(&$this, '_session_write'),
			array(&$this, '_session_destroy'),
			array(&$this, '_session_gc')
			);
		session_start();
	}
	
	
	function _session_open($garbage, $sid)
	{
		return true;
	}

	function _session_close()
	{
		return true;
	}

	function _session_read($sid)
	{		
		$sessionArray = $this->sessionDao->getSessionByIdArray($sid);
		if($sessionArray != null)
		{
			$session = $sessionArray[0];
			return $session->getData();
		}
		return "";
	}
	
	function _session_write($sid, $data)
	{
		$sessionArray = $this->sessionDao->getSessionByIdArray($sid);
		if($sessionArray != null)
		{
			$this->_updateSession($sid, $data);
		}
		else
		{
			$this->_insertSession($sid, $data);
		}
		return true;
	}
	
	function _insertSession($sid = null, $data = null)
	{		
		$session = new SessionEntity();
		if(isset($_SESSION['sessionMemberId']))
		{
			$session->setMemberId($_SESSION['sessionMemberId']); 
		}
		else
		{		
			$session->setMemberId(""); 
		}
		if(isset($_SESSION['sessionPassword']))
		{
			$session->setPassword($_SESSION['sessionPassword']); 
		}
		else
		{		
			$session->setPassword(""); 
		}
		$session->setId($sid);
		$session->setData($data);
		$session->setTimestamp(null);
		$this->sessionDao->insert($session);	
	}

	function _updateSession($sid = null, $data = null)
	{
		$memberId="";
		$password="";
		if(isset($_SESSION['sessionMemberId']))
		{  		
		$memberId = $_SESSION['sessionMemberId'];
		}
		if(isset($_SESSION['sessionPassword']))
		{
			$password = $_SESSION['sessionPassword'];
		}
		$this->sessionDao->updateByIdMemberIdPasswordData($sid, $memberId, $password, $data);
	}
		
	function _session_destroy($sid)
	{
		$this->sessionDao->deleteSessionById($sid);
		return true;
	}

	function _session_gc($maxlife)
	{
		$today = mktime(date("H"), date("i"), date("s"), date("m"), date("d"),  date("Y"));
		$delete_date = $today -  86400 * SESSION_GARBAGE_COLLECT_INTERVAL;
		$this->sessionDao->deleteSessionWithDateBefore(date('Y-m-d H:i:s', $delete_date));
		return true;
	}
}
?>

daoですが、insertメソッドでinsert_NO_PERSISTENT_PROPS = "timestamp"を設定しているのに注意(NO_PERSISTENT_PROPSで設定したフィールドはクエリに含まれない)。どうやら、こうしないとmysqlでtimestampフィールドに挿入した時点の時間が入りません(0000-00-00 00:00:00になってしまう)。また、update時は単純にupdate($entity)といきたいところですが、僕の環境では何故かどうしても挿入できませんでした(謎)。updateByIdMemberIdPasswordDataメソッドでSQLを書いて対処したところ上手くいったので、とりあえずそのままにしています。

  • SessionDao.class.php
<?php

/**
 * @author 
 * @since 2006/06/20
 */
interface SessionDao
{
	const BEAN = "SessionEntity";

//	const update_NO_PERSISTENT_PROPS = "timestamp";	
//	public function update(SessionEntity $entity);
	const updateByIdMemberIdPasswordData_SQL = "update sessions set member_id=/*member_id*/, password=/*password*/, data=/*data*/ where id=/*id*/"; 
	public function updateByIdMemberIdPasswordData($id, $member_id, $password, $data);

	const insert_NO_PERSISTENT_PROPS = "timestamp";
	public function insert(SessionEntity $entity);	

	const deleteSessionById_SQL = "delete from sessions where id = /*id*/"; 
	public function deleteSessionById($id);
	const deleteSessionWithDateBefore_SQL = "delete from sessions where timestamp < /*date*/"; 
	public function deleteSessionWithDateBefore($date);

	const getSessionByIdArray_QUERY = "id = /*id*/"; 	
	public function getSessionByIdArray($id);
}

?>

entityは自動生成のものがほぼそのまま使えますが、TIMESTAMP_PROPERTYアノテーション等が生成されないので記入しておきます(フィールド名がtimestampなのでS2Daoで自動認識してくれるとは思いますが)。逆に、IDアノテーションが自動生成されているのですが、このテーブルはIDをデータベースに生成させないので削除します。

  • SessionEntity.class.php
<?php
/**
 * @author 
 * @since 2006/06/20
 */
class SessionEntity implements Serializable
{
	const TABLE = "sessions";
	
	const id_COLUMN = "id";
	const memberId_COLUMN = "member_id";
	const password_COLUMN = "password";
	const data_COLUMN = "data";
	
	const TIMESTAMP_PROPERTY = "timestamp";
	const timestamp_COLUMN = "timestamp";

	private $id;
	private $memberId;
	private $password;
	private $data;
	private $timestamp;

	public function getId(){return $this->id;}
	public function setId($id){$this->id = $id;}
	public function getMemberId(){return $this->memberId;}
	public function setMemberId($memberId){$this->memberId = $memberId;}
	public function getPassword(){return $this->password;}
	public function setPassword($password){$this->password = $password;}
	public function getData(){return $this->data;}
	public function setData($data){$this->data = $data;}
	public function getTimestamp(){return $this->timestamp;}
	public function setTimestamp($timestamp = null){$this->timestamp = $timestamp;}

	public function toString()
	{
		$buf = "";
		$buf .= "id = " . $this->id . ", ";
		$buf .= "memberId = " . $this->memberId . ", ";
		$buf .= "password = " . $this->password . ", ";
		$buf .= "data = " . $this->data . ", ";
		$buf .= "timestamp = " . $this->timestamp . " {";
		$buf .= "}";
		return $buf;
	}

	public function serialize(){
		$prop = array();
		foreach($this as $key => $value){
			$prop[$key] = $value;
		}
		return serialize($prop);
	}

	public function unserialize($serialized)
	{
		foreach(unserialize($serialized) as $key => $value)
		{
			$this->$key = $value;
		}
	}
	
	public function hashCode() {
		return $this->getId();
	}
}
?>

以上でsessionServiceは出来上がりました。あとはどこでこのserviceを呼び出すかです。memberLoginAction等にこのserviceをDIしてやっても良さそうですが、セッションのスタートはwebアプリケーションの多くの画面で必要な上、ログイン等actionの目的とする処理とは本質的に関係無いですし、各actionにsessionServiceのフィールドを追加するのも面倒です。実はそういった問題を解決するのにぴったりな仕組みがS2Containerにはあります。つまり、AOPの適用機能です。アスペクト指向とか言われても良くわからないのですが、ある処理A,B,C...があって、それらそれぞれの処理の前や後や前後に行いたい処理Dがある、そんな時に便利なのがこの機能、位の気持ちで使ってしまいます。


では実際に作っていきます。interceptor classを作り、それをdiconファイルに設定してaspectで折り込んでやります。まず、S2Baseのinterceptorコマンドでclassを作ります。今回はactionの前にセッションをスタートしたいので、interceptorコマンドのbeforeを指定して生成します。名前はSessionFilterとしておきましょう。LoginSample\app\modules\Login\interceptor以下に生成されますので、あとはbeforeメソッドを実装してやればOKです。処理の内容的には、定数SIDが定義されていればセッションは既にスタートしているので何もしません。さもなければsessionServiceを使ってセッションをスタートします。戻り値をnullとすることで、そのままactionに処理が移ります。

  • SessionFilter.class.php
<?php
/**
 * available properties.
 *    protected $invocation;
 *    protected $request;
 *    protected $moduleName;
 *    protected $actionName;
 *    protected $action;
 *    protected $view;
 *    protected $controller;
 */
class SessionFilter extends S2Base_AbstractBeforeFilter
{
	private $service;

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

	public function before()
	{
		if(defined('SID') == false)
		{
			$this->service->start();
		}
		return null;
	}
}
?>

sessionServiceを使うので、diconファイルを用意してやります。これでSessionFilterの準備は出来ました。

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

	<component name="filter" class="SessionFilter">
		<property name="service">sessionService.service</property>
	</component>
</components>

action diconにaspectの記述を加えることで、SessionFilterを折り込みます。SessionFilter.diconをincludeして、sessionFilter.filterを記述するだけです。以上で、memberLoginActionの処理の前にセッションが開始されるようになりました。

  • 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>
		<property name="dto">dto</property>
		<property name="service">memberLoginService.service</property>
	</component>
	<component name="dto" class="MemberLoginDto">
	</component>
	
</components>

ついで、この前作ったMemberLoginActionをちょっと変更しておきます。ログインが成功したら、セッション変数sessionMemberId, sessionPasswordを登録し、memberId, passwordを代入しておきます。これで、他の画面に遷移しても、ログインしていればセッション変数を使って会員を引き当てられるようになりました。

  • 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->assign('dto', $this->dto);
			return "memberLogin.tpl";
		}
	}

}
?>

続く...