写了一个aop框架已经很久了,也一直想用它完成几个小项目实际测试下效果。最近在写一个微博应用,刚刚有用户的时候,发现需要记录下用户的行为日志。而AOP太适合干记录日志这活了~ 果断修改类内部方法调用为aop方式调用,以提供可编程切面。但是,发现在调用静态方法的时候没有执行切面前后的过滤器。就去找原因,发现并解决之后才有此篇文章的。
PHP在切面编程方面还是很简单的。因为PHP有提供魔术方法__call(),__callStatic()。前者拦截非静态方法,后者拦截静态方法。当然,有人会有疑问,已经存在的方法,调用的时候是不执行魔术方法的啊?你也就没办法拦截了啊!解决这个办法就是新建一个没有任何普通方法的代理类去代理要执行的对象或者类。这样,所有的调用方法都作用到代理类上。也就执行魔术方法了,也就提供了可编程切面。
下边上代码说明我是如何实现的。
首先说简单的对象调用,在实例化对象的时候,同样不能使用new,而是使用控制反转的方式获取代理对象
$userModel = Ioc::loadObject('UserModel');
使用$logModel的时候基本可以当成UserModel的对象使用。为啥说基本呢?因为在你用get_class($userModel)的时候是不会返回UserModel的,而是代理类的名称。而正常我们是无需进行这些操作的,或者可以避免这么写代码。简单的调用例子如下
$userModel->register('张三','男','26');
再说静态方法的调用。例子如下
Ioc::callStatic('UserController', 'login', array($userInfo));
第一个参数为类名,第二个参数为要调用的静态方法名,第三个参数为参数数组。
类内调用的时候,时常会直接写$this->xxx(),或者self::yyy()。但这样写同样还是绕过了代理对象,无法提供切面。所以,类内调用的写法也就需要写成如下方式。
class test{ public function a(){ $this->b('hello'); //无法提供可编程切面 Ioc::call($this, 'b', array('world')); self::c('hello'); //无法提供可编程切面 Ioc::callStatic(__CLASS__, 'c', array('world')); } public function b($arg){ var_dump($arg); } public static function c($arg){ var_dump($arg); } }
调用方法就说这么多。下边看如何实现吧。首先最关键的就是代理类
<?php /** * 对象代理 * @author wclssdn <ssdn@vip.qq.com> * */ class ObjectProxy { /** * 被代理的对象 * @var object */ private $object; /** * 执行静态方法的类名 * @var string */ public static $classname; public function __construct($object = null) { $this->object = $object; } /** * 拦截被调用对象的方法调用 * @param string $method * @param array $args * @return mixed * @throws Exception */ public function __call($method, array $args) { $reflectionObject = new ReflectionObject($this->object); try { $reflectionMethod = $reflectionObject->getMethod($method); }catch (ReflectionException $e){ throw new Exception('method ' . $method . ' not exists in ' . get_class($this->object)); } if (!$reflectionMethod->isPublic()){ throw new Exception('method ' . $method . ' not public in ' . get_class($this->object)); } //AOP执行目标调用 $aop = new Aop(array($this->object, $method), $args); return $aop->execute(); } /** * 拦截静态方法调用 * @param string $method * @param array $args */ public static function __callstatic($method, array $args){ $classname = self::$classname; //AOP执行目标调用 $aop = new Aop("{$classname}::{$method}", $args); return $aop->execute(); } }
这次更新的就是__callStatic方法。也没加方法是否存在的判断。
其次是控制反转的实现
<?php /** * 控制反转 * @author wclssdn <ssdn@vip.qq.com> * */ class Ioc { /** * 对象缓存 * @var array */ private static $objects = array(); private function __construct() { } private function __clone() { } /** * 加载代理对象 * @param string $classname * @param array $args * @param string $sign 如不想使用单例对象, 使用不同的sign区分不同实例 * @return object */ public static function loadObject($classname, array $args = array(), $sign = '') { $sign = $classname . md5(serialize($args)) . $sign; if (!isset(self::$objects[$sign])){ $r = new ReflectionClass($classname); $constructor = $r->getConstructor(); if ($constructor !== null && $constructor->getParameters()){ if (empty($args) && file_exists($conFile = PATH_CONFIG . 'ioc/' . $classname . '.conf.php')){ $args = include $conFile; } $object = $r->newInstanceArgs($args); }else{ $object = $r->newInstance(); } self::$objects[$sign] = self::getProxy($object); } return self::$objects[$sign]; } /** * 提供切面,执行对象的方法 * @param object $object * @param string $method */ public static function call($object, $method, $args = array()){ if (!is_object($object)){ return false; } return self::getProxy($object)->$method($args); } /** * 提供切面, 调用静态方法 * @param string $classname * @param string $method * @param array $args */ public static function callStatic($classname, $method, $args = array()){ if (!is_scalar($classname)){ return false; } $proxy = self::getProxy(new stdClass()); $proxy::$classname = $classname; return call_user_func_array("ObjectProxy::{$method}", $args); } /** * 获取对象的代理对象 * @param object $object * @return ObjectProxy */ private static function getProxy($object) { return new ObjectProxy($object); } }
控制反转类提供对象的缓存(其实是必要的)同一个类的实例可以始终保持一个。也提供同类同时存在多对象的支持(使用不同参数实例的对象就会是不同的实例了,当然相同的参数就需要手动指定新实例了)。
切面至此已经出来了,针对切面编程就可以任意发挥了。