なんだか間があいてしまいましたが、S2Base.PHP5をなでて参ります。
今回はInterceptor編、AOPってやつですね。
用語の解説は専門のサイトにお任せすることにして、
AOPっていうのは、OOPでいうところのクラスに横断することを集約してしまおうという考え方だと思ってます。
例えば、デバッグの為にメソッドの引数と戻り値をログ出力することを考えるとします。
これをひとつひとつのクラスのメソッドに実装していったら大改修です。
そこで引数と戻り値をログ出力する機能をAOPでいうところのAdviceとして実装して
対象となるクラスの任意の場所(Jointpoint)の集合(Pointcut)を定義して
AdviceをPointcutに対して適応(Weave)するのです。
さて、さっそく実践。
S2Container.PHP5では、AdviceをInterceptorとして実装します。
で、Interceptorを適応するにはdiconのcomponent要素にaspect要素を定義するか
クラスにAnnotationを記述するか
S2ContainerApplicationContextの自動アスペクトを使います。
今回はS2ContainerApplicationContextの自動アスペクトを使います。
S2Container.PHP5には元々組み込まれているInterceptorがあるのですが
せっかくなので自作いたしましょう。
せっかくついでなので、前のエントリに書いたEntityのValidationを行うInterceptorを使います。
このIntercetptorは、Daoに対して適応してinsert、updateといった更新系のメソッドが実行される際に
引数として渡されたEntityに値が設定されているかどうかをチェックして、
値が入っていなければ、例外を発行します。
Entityに値が必要かどうかを定義するのにプロパティ名_REQUIREDという定数をEntityに定義することにします。
このなでるシリーズではS2BasePluginクラスがEntityです。
では早速S2BasePluginクラスのコード。
<?php class S2BasePlugin { const TABLE = "s2base_plugin"; public function __construct(){} protected $id; const id_COLUMN = "id"; public function setId($val){$this->id = $val;} public function getId(){return $this->id;} protected $name; const name_COLUMN = "name"; const name_REQUIRED = "true"; public function setName($val){$this->name = $val;} public function getName(){return $this->name;} public function __toString() { $buf = array(); $buf[] = 'id => ' . $this->getId(); $buf[] = 'name => ' . $this->getName(); return '{' . implode(', ',$buf) . '}'; } /* private $prop; const prop_RELNO = 0; const prop_RELKEYS = 'this_fk:other_pk'; public function setProp(OtherEntity $entity){ $this->prop = $entity; } public function getProp(){ return $this->prop; } */ }一行増えただけですね。
プロパティnameは、必ず値が必要であるということを定義しました。
続いてInterceptorを作成します。
s2baseのコンソールを起動して、Interceptorを作成します。
作成するInterceptorの名前はEntityValueValidatorInterceptorとします。
[ Command list ] 0 : (exit) 1 : dao 2 : dicon 3 : entity 4 : goya 5 : interceptor 6 : module 7 : service choice ? : 5 [ Module list ] 0 : (exit) 1 : sample_module choice ? : 1 interceptor class name ? : EntityValueValidatorInterceptor [ generate information ] module name : sample_module interceptor class name : EntityValueValidatorInterceptor confirm ? (y/n) : yこれでEntityValueValidatorInterceptorが
sampleプロジェクトのapp/modules/sample_module/interceptorディレクトリに作成されました。
作成されたEntityValueValidatorInterceptorは、こんな感じです。
<?php class EntityValueValidatorInterceptor extends S2Container_AbstractInterceptor { /** * @param S2Container_MethodInvocation $invocation * - $invocation->getThis() : return target object * - $invocation->getMethod() : return ReflectionMethod of target method * - $invocation->getArguments() : return array of method arguments */ public function invoke(S2Container_MethodInvocation $invocation) { return $invocation->proceed(); } }EntityValueValidatorInterceptor::invoke()がJoinpointで実行されます。
今は、元々の処理を実行して結果を返すという内容になっています。
で、このEntityValueValidatorInterceptorをこんな感じに変更します。
<?php class EntityValueValidatorInterceptor extends S2Container_AbstractInterceptor { /** * column annotation sufix * */ const COLUMN_ANNOTATION_SURFIX = "_COLUMN"; /** * value required surfix * @var string */ const REQUIRED_ANNOTATION_SURFIX = "_REQUIRED"; /** * Getter name prefix * @var string */ const GETTER_PREFIX = "get"; /** * @param S2Container_MethodInvocation $invocation * - $invocation->getThis() : return target object * - $invocation->getMethod() : return ReflectionMethod of target method * - $invocation->getArguments() : return array of method arguments */ public function invoke(S2Container_MethodInvocation $invocation) { $args = $invocation->getArguments(); $className = null; $reflectionClass = null; $getterName = null; $getterMethod = null; $getterResult = null; foreach($args as $arg) { $className = get_class($arg); if ($className != false) { $reflectionClass = new ReflectionClass($className); //is entity? if(strpos($reflectionClass->getFileName(), S2BASE_PHP5_ENTITY_DIR) !== false) { //get props foreach($reflectionClass->getProperties() as $prop) { //is column of table? if($reflectionClass->hasConstant($prop->getName() . self::COLUMN_ANNOTATION_SURFIX)) { //getter exist? $getterName = self::GETTER_PREFIX . $prop->getName(); if($reflectionClass->hasMethod($getterName)) { $getterMethod = $reflectionClass->getMethod($getterName); if($getterMethod->isPublic()) { //value required? if($reflectionClass->hasConstant($prop->getName() . self::REQUIRED_ANNOTATION_SURFIX)) { $getterResult = $getterMethod->invoke($arg); //value exist? if($getterResult !== 0 && $getterResult == null) { throw new Exception($prop->getName() . " is required."); } } } } } } } } } return $invocation->proceed(); } }定数はそれぞれ以下の様な役割です。
COLUMN_ANNOTATION_SURFIXはS2DaoのCOLUMNアノテーションを定数で行う場合の命名規則です。
REQUIRED_ANNOTATION_SURFIXは先程書いたフィールドが必須かどうかを表す規則です。
GETTER_PREFIXはプロパティのGetterメソッドの命名規則です。
EntityValueValidatorInterceptor::invoke()の中身は
まずメソッドの引数を取得($methodInvocation->getArguments();)
引数のクラス名を取得して($className = get_class($arg);)
引数がクラスかどうかを判断。(if ($className != false))
クラスであるならReflectionを取得して($reflectionClass = new ReflectionClass($className);)
Entityかどうかを判断。(if(strpos($reflectionClass->getFileName(), S2BASE_PHP5_ENTITY_DIR) !== false))
EntityであるならプロパティのReflextionを取得してプロパティごとにループ。
(foreach($reflectionClass->getProperties() as $prop) {)
プロパティがテーブルのカラムと関連付けられているかを判断して
(if($reflectionClass->hasConstant($prop->getName() . self::COLUMN_ANNOTATION_SURFIX)) {)
プロパティのGetterメソッド名を取得して($getterName = self::GETTER_PREFIX . $prop->getName();)
Getterメソッドがあるかどうか判断。(if($reflectionClass->hasMethod($getterName)) {)
Getterメソッドがpublicかどうかを判断して、(if($getterMethod->isPublic()) {)
値が必要かどうかを判断。(if($reflectionClass->hasConstant($prop->getName() . self::REQUIRED_ANNOTATION_SURFIX)) {)
値が必要なら、GetterメソッドのReflectionを実行。
($getterResult = $getterMethod->invoke($arg);)
値がなければ例外発行。
という流れです。
では早速このInterceptorをDaoに適応します。
sample_moduleモジュールの設定ファイルが
sampleプロジェクトのapp/modules/sample_module/sample_module.inc.phpにあります。
ここでS2ContainerApplicationContextの設定を行っていますので、
このファイルを変更します。
オリジナルはこんな感じです。
<?php S2ContainerApplicationContext::init(); S2ContainerApplicationContext::import(dirname(__FILE__) . '/dao'); S2ContainerApplicationContext::import(dirname(__FILE__) . '/entity'); S2ContainerApplicationContext::import(dirname(__FILE__) . '/interceptor'); S2ContainerApplicationContext::import(dirname(__FILE__) . '/service'); S2ContainerApplicationContext::import(S2BASE_PHP5_ROOT . '/app/commons/dao'); S2ContainerApplicationContext::import(S2BASE_PHP5_ROOT . '/app/commons/dicon/dao.dicon'); S2ContainerApplicationContext::registerAspect('/Dao$/', 'dao.interceptor'); ?>前回も話しましたが、すでにDaoに対してdao.interceptorが定義されています。
で、このファイルをこう修正します。
<?php S2ContainerApplicationContext::init(); S2ContainerApplicationContext::import(dirname(__FILE__) . '/dao'); S2ContainerApplicationContext::import(dirname(__FILE__) . '/entity'); S2ContainerApplicationContext::import(dirname(__FILE__) . '/interceptor'); S2ContainerApplicationContext::import(dirname(__FILE__) . '/service'); S2ContainerApplicationContext::import(S2BASE_PHP5_ROOT . '/app/commons/dao'); S2ContainerApplicationContext::import(S2BASE_PHP5_ROOT . '/app/commons/dicon/dao.dicon'); S2ContainerApplicationContext::registerAspect('/Dao$/', 'EntityValueValidatorInterceptor', 'insert,update'); S2ContainerApplicationContext::registerAspect('/Dao$/', 'dao.interceptor'); ?>S2ContainerApplicationContext::registerAspect()を使って
正規表現でDaoという文字列で終わるクラスのinsertとupdateというメソッドに(Pointcut)
EntityValueValidatorInterceptor(Advice)を適応(Weave)しています。
さて、せっかくなのでUnitTestを実行して、Interceptorが動作しているかどうか確かめてみましょう。
sampleプロジェクトのtest/modules/sample_module/dao/S2BasePluginDaoTest.phpを編集します。
<?php class S2BasePluginDaoTest extends PHPUnit_Framework_TestCase { private $module = 'sample_module'; private $container; private $dao; private $pdo; public function __construct($name) { parent::__construct($name); } public function testInsert() { $plugin = new S2BasePlugin(); try { $this->dao->insert($plugin); $this->fail("Invalid insert!"); } catch (Exception $e) { } $plugin->setName("TestPlugin"); try { $this->dao->insert($plugin); } catch (Exception $e) { $this->fail("Invalid insert!"); } } public function testUpdate() { $plugin = new S2BasePlugin(); $serviceResult = $this->dao->findAllList(); $iterator = $serviceResult->iterator(); $plugin = $iterator->current(); $plugin->setName(null); try { $this->dao->update($plugin); $this->fail("Invalid update!"); } catch (Exception $e) { } $plugin->setName("TestPlugin"); try { $this->dao->update($plugin); } catch (Exception $e) { $this->fail("Invalid update!"); } } public function setUp(){ print __CLASS__ . '::' . $this->getName() . PHP_EOL; $moduleDir = S2BASE_PHP5_ROOT . "/app/modules/{$this->module}"; require_once($moduleDir . "/{$this->module}.inc.php"); $this->container = S2ContainerApplicationContext::create(); $this->dao = $this->container->getComponent('S2BasePluginDao'); $dataSource = $this->container->getComponent("dataSource"); $this->pdo = $dataSource->getConnection(); $this->pdo->beginTransaction(); } public function tearDown() { print PHP_EOL; $this->pdo->rollBack(); $this->pdo = null; $this->container = null; $this->dao = null; } }S2BasePluginDaoTest::testInsert()でinsert時の動作を、
S2BasePluginDaoTest::testUpdate()でupdate時の動作を試験しています。
では早速UnitTestを実行。 sampleプロジェクトディレクトリ内で以下のコマンドを発行します。
% s2base test DaoDaoという名前を含むTestを実行する、という意味です。
すると結果は
PHPUnit 3.1.8 by Sebastian Bergmann. S2BasePluginDaoTest::testInsert .S2BasePluginDaoTest::testUpdate . Time: 0 seconds OK (2 tests)テストも完了です。
S2Baseの基本となるmodule、Entity、Dao、Service、Interceptorを一通りなでましたので、
これでひとまずS2Baseをなでるシリーズは完結です。
S2Base2.0系から導入されたS2ContainerApplicationContextがでいい仕事をしてます。
Interceptorの適応も含めたdiconレスなコンポーネント定義で定義ファイルから開放されます。
ということで次回以降のSeasar.PHPをなでるシリーズは
S2ContainerApplicationContextをなでてみようかな、とか思っています。