AOP是时下比较流行的一种编程思想,它为程序的解耦带来了进一步的发展。在出现AOP的应用之前,我们如果需要在每个程序执行前或执行后记录LOG,在执行每个代码前进行用户访问控制,就不得不重复的显式调用同一个方法。如
public void anyfunc(){ createLog(); /*some logic code*/ }
或
public void anyfunc(){ if(RBAC::access()){ /*some logic code*/ } }
这些“方面”的代码会显式的存在与每个需要它的方法中,与业务代码形成了紧耦合的关系。而AOP的出现解决了固定需求的代码与普通的业务代码之间的隔离。为程序提供了更高的可扩展性与可修改性,更加符合面向对象编程的原则。在现今流行的应用于WEB开发的语言中,Java通过动态代理技术原理可以很好的实现AOP的思想。Python也可以通过@修饰符来实现AOP。而PHP确不能通过语言本身的特性来实现AOP的需求。
对于PHP先天性的弱势,许多PHP框架也采取了一些措施,如Yii框架提供了beforeControllerAction与afterControllerAction方法,支持代码执行时的before与after操作。但与JAVA以注释的实现方式相比,还是缺少了一定的灵活性。那么PHP能不能也通过注释的方式来实现AOP呢?之前我一直认为除非PHP的语言特性支持,否则要想通过注释的方式来实现AOP是不可能的。但当我在一次工作中接触到一款名为OpenCart的开源电子商店的代码后,受到了一点启发。其实,我们完全可以通过重写页面的方法来“实现”注释方式的AOP呀。下面,我将以自己做的一个demo样本,与大家一起探讨下PHP的”注释AOP“实现,希望能对大家有一个启发。此段demo是在Yii框架下实现的,里面还有一些问题需要进行优化和完善。
首先给出此方法的一个逻辑流程:
思想很简单,就是根据注释,按照一定规则生成新的代码文件,然后让程序执行新生成的文件代码。由于每次操作都需要生成新的文件,这样对程序的执行效率会产生一定的影响,所以我们可以考虑按一定的策略来生成新文件,比如按照文件的修改时间来生成新文件,在执行相应操作时先判断controller文件有无修改,如果无修改,则读取上次生成的文件执行,如在上次执行的间隔时间中,controller文件有修改,则生成新的文件。
在样例中,我们主要支持before和after的操作,before是在controller方法执行前执行,after是在controller方法执行后执行。举个例子,有以下代码:
/*@before_Logger_createlog*/ public function logic(){ echo 'Hello World'; }
那么在执行logic()方法时,会先执行Logger类的createlog方法记录log,log方法不会渗透到logic()方法的实体内,而是以注释的方式存在。下面我们就来看看实现这项需求的具体代码。代码实现步骤:
一、找到CWebApplication.php文件,它的位置是\framework\web\CWebApplication.php。添加parseClassFile方法用来解析原有的Controller类,代码如下:
public function parseClassFile($id,$aopconfigpath){ $className=ucfirst($id).'Controller'; $classFile=$this->getControllerPath().DIRECTORY_SEPARATOR.$className.'.php'; if(!is_file($classFile)){ return false; } $originalContent = file_get_contents($classFile); //获取原始Controller的内容 $mark_patten = '/\/\*@(before_\w+|after_\w+)\*\//s'; $mark_patten1 = '/(\/\*@before_[^\*]+\*\/|\/\*@after_[^\*]+\*\/)/s'; $mark_patten2 = '/(\/\*@before_[^\*]+\*\/[^\d]|\/\*@after_[^\*]+\*\/[^\d])/s'; preg_match_all($mark_patten1, $originalContent,$mark,PREG_OFFSET_CAPTURE); if(empty($mark[0])){ return $originalContent; //如果没有找到相关的AOP注释,则直接返回原始内容 } $newContent = $originalContent; //以下是具体解析注释的过程 foreach($mark[0] as $k=>$m){ if($k!=0){ preg_match_all($mark_patten2, $newContent,$submark,PREG_OFFSET_CAPTURE); $m[1] = $submark[0][0][1]; $m[0] = $submark[0][0][0]; } $front_part = substr($newContent,0,$m[1]); $behind_part = substr($newContent,$m[1]+strlen($m[0])); $replace_part = preg_replace('/\s+/', '', $m[0]).$m[1]; $exact_pattens[] = $replace_part; $newContent = $front_part.$replace_part."\n".$behind_part; } foreach($exact_pattens as $k=>$exact_patten){ $exact_patten = addcslashes($exact_patten,'/*'); preg_match('/'.$exact_patten.'([^\{]+)/s',$newContent,$fun); $start = stripos($newContent,$fun[1])+strlen($fun[1]); $step1 = substr($newContent,$start); preg_match_all('/(public\r*\n*\s+function\r*\n*\s+|protected\r*\n*\s+function\r*\n*\s+|private\r*\n*\s+function\r*\n*\s+)/s',$step1,$fun_mark,PREG_OFFSET_CAPTURE); $count_fun_mark = count($fun_mark[0]); $fun_mark_arr = array(); for($i=0;$i<$count_fun_mark;$i++){ $fun_mark_arr[] = $fun_mark[0][$i][1]; } sort($fun_mark_arr,SORT_NUMERIC); $nearly_fun_position = $fun_mark_arr[0]; $step2 = substr($step1,0,$nearly_fun_position); $code_start_position = strpos($step2,'{'); $code_end_position = strripos($step2,'}'); $find_fun_patten = '/@\w+_(\w+)_(\w+)/'; preg_match($find_fun_patten, $exact_patten,$funname_arr); $command = 'Yii::app()->'.$funname_arr[1].'->'.$funname_arr[2].'();'; //需要执行方面的代码 $insert_part = $command; if(strpos($exact_patten,'before_')){ $front_part = substr($step2, 0,$code_start_position+1); $behind_part = substr($step2,$code_start_position+1); } else if(strpos($exact_patten,'after_')){ $front_part = substr($step2,0,$code_end_position); $behind_part = substr($step2,$code_end_position); } $replace_part = $front_part.$insert_part.$behind_part; $replace_parts[] = $replace_part; $need_replace_parts[] = $step2; } $result = str_replace($need_replace_parts, $replace_parts, $originalContent); //生成解析好的Controller内容 return $result; }
此代码只是简单的实现了注释的解析工作,并且注释不用用于Controller类中的最后一个方法(算是一个小BUG)。解析后的新内容其实还可以进行进一步的压缩代码操作以节省文件空间,对代码的执行效率也有一定的好处。
二、在CWebApplication.php文件内添加createCompilerController以生成新的Controller文件。代码如下:
public function createCompilerController($classname,$basepath,$controllercontents){ if($controllercontents==false){ return false; } $compiler_file = $basepath.DIRECTORY_SEPARATOR.$classname.'.php'; file_put_contents($compiler_file, $controllercontents); return true; }
此代码只是简单的实现了文件生成的功能,实际上应该根据文件的具体修改时间来判断是否应该生成新的文件,除第一次生成外,只有原始Controller的代码有过更改才应该生成新的文件,否则就应该用上一次生成的Controller文件。
三、在CWebApplication.php文件的createController方法内找到以下代码:
if(!isset($basePath)) // first segment { if(isset($owner->controllerMap[$id])) { return array( Yii::createComponent($owner->controllerMap[$id],$id,$owner===$this?null:$owner), $this->parseActionParams($route), ); } if(($module=$owner->getModule($id))!==null) return $this->createController($route,$module); $originalBasePath = $basePath=$owner->getControllerPath(); $controllerID=''; } else $controllerID.='/'; $className=ucfirst($id).'Controller';
在下方添加以下的逻辑代码:
$change_read_controller_flag = false; //框架是执行新的Controller文件还是执行原始Controller文件,true代表执行新的Controller文件。 if($controllerID==''){ $aopConfigPath = $this->getAopConfigPath(); $controllerContents = $this->parseClassFile($id,$aopConfigPath); $basePath = $this->getCompilerControllerPath(); if($this->createCompilerController($className,$basePath,$controllerContents)){ //如果创建新文件成功,则框架读取新的Controller文件。 $change_read_controller_flag = true; } } if(!$change_read_controller_flag){ $basePath = $this->getControllerPath(); } $classFile=$basePath.DIRECTORY_SEPARATOR.$className.'.php';
四、在\project\protected\components下建立AOP类,如Logger类及相应的方法,并在\project\protected\config\main.php配置文件注册此类。
五、在\project\protected\controllers的Controller类中找到需要进行before或after的方法,在该方法前添加形如/*@before_Logger_createlog*/的注释。在实际的用户请求中,就会在执行相应的controller方法前或后(取决于注释的关键字是before还是after)执行切面类的方法了。
虽然通过代码重构的技术,可以实现PHP的注释AOP功能,但此种方法也有一些缺点,如I/O操作带来的性能消耗。当Controller文件占用空间较大时,会对执行工作效率产生严重的影响。另外如果要让注释支持更多的功能,则需要提供更为复杂的解析方式,解析是通过正则表达式与替换、字符串查找方式来进行的,而这些操作对系统的性能也有一定影响。所以在进行解析时应尽量避免内嵌循环遍历等操作。样例中的代码还有许多地方需要修改和优化,希望此篇文章能为大家起到抛砖引玉的作用。