自动判断应该Ajax还是return

起因

最近回顾以前的代码,发现一个偶尔会见到的现象。一个类里面的方法可能需要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';
        //    AdminControllerProController
        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正是调用者的函数名和类名。

debug_backtrace

封装好的函数代码如下:

    //是否为当前模块下的控制器下的方法,常用于判断是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 的类名将会是 HomeProCtrl,也就是得到的是父类的名字而不是自己的名字,这个问题用 __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() 方法自动判断返回呢。

原文地址:https://www.cnblogs.com/batsing/p/reJax.html