起因
最近回顾以前的代码,发现一个偶尔会见到的现象。一个类里面的方法可能需要Ajax返回,也有可能需要函数return。这个现象发生在网站MVC中的 逻辑层(或模型层),示例如下。IndexCtrl是控制器负责渲染页面,ProCtrl是逻辑器负责读取处理数据,A函数是实例化一个类,M函数是读取数据表的意思。现在只是简单的页面输出。
class IndexCtrl extends Ctrl{ function index(){ $proList = A(‘Pro‘)->getList(); $this->assign(‘proList‘,$proList) ->display(); } } class ProCtrl extends Ctrl{ function getList(){ $proList = M(‘pro‘)->where("isMain=‘1‘")->select(); return $proList; } }
现在来了一个管理后台中,需要用Ajax获取这些首页产品列表。怎么改呢?直接再加一个 function getListAjax(); 然后读取同样的数据库,做同样的操作?这显然不科学,同样的逻辑不应该被实现两次。
那就定义一个函数 getListAjax() 里面调用自身的 getList() 然后再Ajax返回。这样看起来似乎也没太大问题,但是当这种类似的场景增多的时候,岂不是所有的方法都有一个ajax的副本?
这样的话,就应该对 getList 函数进行改造了。怎么改呢? 加一个可选参数 $isReturn 默认值为FALSE,此时为AJAX请求返回JSON;调用者调用时传入参数值为TRUE,函数进行return。代码如下:
class IndexCtrl extends Ctrl{ function index(){ $proList = A(‘Pro‘)-> getList( TRUE ); $this->assign(‘proList‘,$proList) ->display(); } } class ProCtrl extends Ctrl{ function getList( $isReturn=FALSE ){ $proList = M(‘pro‘)->where("isMain=‘1‘")->select(); if($isReturn){ return $proList; }else{ $this->ajaxReturn($proList); } } }
这样的话,基本上实现了一个函数可以同时拥有两种返回方式,一种是AJAX,一种是函数return。但是这存在很多明显的问题:
1、需要修改调用者,调用者需要传递参数TRUE才能实现函数返回;
2、如果该函数本身就有参数,那加上这个附加参数就会显得很臃肿。
3、不自动,基本属于重复手写状态。
寻觅
根据观察到的现象,我们发现这里判断的关键是 $isReturn 变量,这个变量是true还是false到底有没有办法做到自动识别呢?那么自动识别的前提是什么呢?根据项目的实际情况,我定出了规则 ,就是 直接访问这个方法的URL(如http://localhost/Pro/getList)则使用AJAX返回,访问其它URL则使用函数return。
那怎么做呢?项目中使用的是Thinkphp框架,它里面有几个预定义魔法常量 MODULE_NAME,CONTROLLER_NAME,ACTION_NAME,分别表示当前访问的模块名(Home、Admin、Mobile等)、控制器名、操作名。而PHP原生也有几个魔法常量,__CLASS__、__FUNCTION__表示当前访问的类名和方法名。所以只要判断 模块名+控制器名==类名、操作名==方法名,就可以得出是URL直接访问的,使用AJAX返回,否则使用return。代码如下:
class ProCtrl extends Ctrl{ function getList(){ $proList = M(‘pro‘)->where("isMain=‘1‘")->select(); $ctrlName = MODULE_NAME.‘\Controller\\‘.CONTROLLER_NAME.‘Controller‘; // Admin\Controller\ProController if($ctrlName==__CLASS__ && ACTION_NAME==__FUNCTION__){ $this->ajaxReturn($proList); }else{ return $proList; } } }
封装
这样就完了吗?当然不是,这么长的一个判断总不能每个都复制一遍吧,肯定要把它封装起来成为一个公共函数。这个封装看起来很简单嘛,直接弄去一个函数里就完了。然而,too native!因为PHP的这两个魔术常量是会变的。当你把这段代码抽到一个函数中时(例 isNowAction() ),__CLASS__ 就变成了空字符串,__FUNCTION__ 就变成了 ‘isNowAction‘ 。好吧,既然普通函数不行,那有没有别的方法呢?
一、C语言中有一个叫做“内联函数”的概念,就是说这个函数虽然是写出来了,但是编译的时候是把它作为调用者的一部分直接编译到该函数中,而普通函数是通过返回跳转的(如汇编指令 RET 、 JMP)。所以,按理说这样的话这两个魔术常量就会像预期一样得出我们想要的值。然而,PHP里面并没有内联函数这个东西,并没有 inline 关键字 。。。
二、一技不成又生一技,有一个东西就做“宏定义”。C语言里面很多时候是用来封装一个小函数的,比如 #define pyth(x,y) sqrt(x*x+y*y) ,可以这样用来宏定义一个函数,或者说是简写一个函数。所以我也按照这样的方式写了一个(用了匿名函数,这很JS)
define(‘isNowAction()‘, function(){ $ctrlName = MODULE_NAME.‘\Controller\\‘.CONTROLLER_NAME.‘Controller‘; if($ctrlName==__CLASS__ && ACTION_NAME==__FUNCTION__){ return TRUE; }else{ return FALSE; } });
但是发现这个并没有成功执行。翻了一下PHP的文档才知道这PHP的define并不能定义函数
还真不得不说,有时候特性太少真是一个麻烦事啊。难道这样就没有办法了吗?在函数里设两个参数,让调用者把__CLASS__和__FUNCTION__传过来?但这样的封装只是聊胜于无,并不理想。或者可以这样想,既然没有办法定义特殊的函数,那能不能有办法在函数里翻出调用者的信息呢?
功夫不负,还真有!有一个函数名为 debug_backtrace() ,可以找到调用者的信息。如图中红色箭头所指, 这个数组的第二项中的function和class正是调用者的函数名和类名。
封装好的函数代码如下:
//是否为当前模块下的控制器下的方法,常用于判断是return还是ajax function isNowAction(){ // var_dump( debug_backtrace() ); $ctrlName = MODULE_NAME.‘\Controller\\‘.CONTROLLER_NAME.‘Controller‘; $className = debug_backtrace()[1][‘class‘]; $funcName = debug_backtrace()[1][‘function‘]; if($ctrlName==$className && ACTION_NAME==$funcName){ return TRUE; }else{ return FALSE; } }
当然还可以直接封装到逻辑层Ctrl基类中,作为一个基础方法
class Ctrl { ......//框架原来写好的一些代码 protected function reJax($data){ $ctrlName = MODULE_NAME.‘\Controller\\‘.CONTROLLER_NAME.‘Controller‘; $className = debug_backtrace()[1][‘class‘]; $funcName = debug_backtrace()[1][‘function‘]; if($ctrlName ==$className && ACTION_NAME==$funcName){ $this->ajaxReturn($data); }else{ // echo ‘return‘; return $data; } } }
这样的话 ProCtrl 可以非常简洁,而又能自动判断是应该AJAX返回还是return
class ProCtrl extends Ctrl{ function getList(){ $proList = M(‘pro‘)->where("isMain=‘1‘")->select(); return $this->reJax($proList); } }
继承问题
本来以为,这个函数到此为止就算是结束了。然而并没有。为什么呢?继承的问题。比如 Admin模块里的 ProCtrl类的getList()方法 继承自Home中的ProCtrl类,那么debug_backtrace() 得出来的 class 的类名将会是 Home\ProCtrl,也就是得到的是父类的名字而不是自己的名字,这个问题用 __CLASS__ 魔法变量也是一样存在。不知这个算不算是PHP语言的一个BUG呢?
再看到PHP文档中下面的评论,确实有说到__CLASS__的这个问题,而与此很类似的代替方法是使用 get_class($this) 函数。注意到这里有个参数是$this,也就是说当前类,所以最终我们的这个封装的方法只能写在 Controller 基类中,而无法写在公共函数方法中。最终代码如下:
class Ctrl { ......//框架原来写好的一些代码 protected function reJax($data){ $ctrlName = MODULE_NAME.‘\Controller\\‘.CONTROLLER_NAME.‘Controller‘; $className = get_class($this); $funcName = debug_backtrace()[1][‘function‘]; if($ctrlName ==$className && ACTION_NAME==$funcName){ $this->ajaxReturn($data); }else{ // echo ‘return‘; return $data; } } }
再说两句
万万没想到,想要实现一个如此基础的自动化功能,扯出了这么多一堆概念和技术,从C语言、汇编到PHP再到对象的问题,中间还有类似JS的影子。
最后,可能有人会问,折腾了这么久,这个到底有多大的用处呢?如果同时有 网页版(需要页面渲染) 和 APP(需要JSON数据),同一个功能是写两份代码呢,还是直接使用这个 reJax() 方法自动判断返回呢。