thinkphp 框架适配修改

3,159 阅读7分钟
原文链接: dssysway.github.io

最近项目需要使用php开发,php开发中比较流行的业务框架有Laravel, yii, CakePHP. 最后决定使用ThinkPhp的原因是上述三种并没有用过,为了支持国产就直接ThinkPHP 搞起了。裸用php同样可以完成业务开发。但是业务上涉及到两三位后台同学 一起开发项目,时间上也比较紧.因此有必要借助框架约束来统一后台开发行为, 另外可以方便借助框架提供的路由,安全配置,ORM等通用功能提高研发效率 本文讨论的ThinkPHP仅限于3.2版本.


ThinkPHP的代码非常简洁,基本上对于拥有入门级别的php开发都能看得懂。 改起来相对方便。在业务开发过程中,笔者发现PHP中的某些地方并不能完全满足 项目开发的需求,因此在参照内部系统的基础上对框架代码进行了补充修改。 所做修改已经在线上顺利跑过了。下面把修改点罗列下,为以后的项目做下记录 和经验积累.


日志api的修整

ThinkPHP 提供了Log::record()的方法,官方建议使用该方法来记录错误日志, 该方法会将日志写入到内存中,在请求结束的回调中执行写磁盘存文件或者db操作。 但是业务开发过程中,日志信息根据日志的产生来源分为以下四种:

  • 用户行为日志
  • 业务系统日志
  • 统计日志
  • 服务器行为日志

用户行为日志通常跟具体的用户ID关联在一起,用来描述用户行为产生的关联结果。 现网用户投诉信息大多数从该类型的日志加以筛选和跟踪.在游戏后台系统中 这类日志通常会专门存储起来方便请求关键上下文保存,叫做tlog 业务系统日志通常作为用户行为日志的补充,更多的是关联请求接口的行为。 通常可以记录业务系统各种异常或者错误情况 统计日志用于多种统计行为,类似于接口请求耗时,在线人数,访问人数等业务系统指标 服务器行为日志用于记录跟业务逻辑无关的日志行为,更多记录的是业务系统与 操作系统发生关联部分的日志信息。类似于进程的CPU占用,内存占用,共享内存占用, 进程重启,reload,升级等日志信息。

线上的日志系统最好兼顾以下几个点:

  1. 最好日志系统可以做到支持上述日志的分类的打印,
  2. 日志信息打印的时候最好跟代码上下文关联起来方便问题定位,
  3. 日志信息最好能够打标签,像Android那样打个tag来区分某些日志信息
  4. 日志信息最好能够区分打印级别,做到可配置化

ThinkPHP除了满足第四点外,并不支持其他三点。因此对日志api做了修改以支持 上述三点, 同时满足根据日志来源区分打印。下面贴主要代码:

   <?php
    function GET_CODE_INFO()
    {

        $debugInfo = debug_backtrace();
        if (isset($debugInfo)) {

            $fileLocation = $debugInfo[1]['file'];
            $fileName = strrchr($fileLocation, '/');
            return $fileName . ":" .
                $debugInfo[1]['line'] . " | ";
        }
    }
    /
     * SVR LOG define
     * @param [type] $msg [description]
     */
    function SVRLOG_WARN($msg, $tag = '')
    {

        if (empty($msg)) return;
        Log::myRecord('svr', GET_CODE_INFO() . $msg, Log::WARN, $tag);
    }
    function SVRLOG_ERR($msg, $tag = '')
    {

        if (empty($msg)) return;
        Log::myRecord('svr', GET_CODE_INFO() . $msg, Log::ERR, $tag);
    }
    /
    * [USR LOG  define
    * @param [type] $msg [description]
    * @param [type] $tag [description]
    /
    function USRLOG_WARN($msg, $tag = '')
    {

        if (empty($msg)) return;
        Log::myRecord('user', GET_CODE_INFO() . $msg, Log::WARN, $tag);
    }
    function USRLOG_ERR($msg, $tag = '')
    {

        if (empty($msg)) return;
        Log::myRecord('user', GET_CODE_INFO() . $msg, Log::ERR, $tag);
    }
    /**
    * [STATIS LOG  define
    * @param [type] $msg [description]
    * @param [type] $tag [description]
    /
    function STATISLOG_WARN($msg, $tag = '')
    {

        if (empty($msg)) return;
        Log::myRecord('statis', GET_CODE_INFO() . $msg, Log::WARN, $tag);
    }
    function STATISLOG_ERR($msg, $tag = '')
    {

        if (empty($msg)) return;
        Log::myRecord('statis', GET_CODE_INFO() . $msg, Log::ERR, $tag);
    }
    ?>


上述代码提供了统计日志,服务器日志,以及用户日志的调用接口. 在record实现的基础上增加了日志打印与代码位置的关联信息, 日志来源信息(svr, user, statis)。同时支持用户输入tag 下面来看看myRecord部分的修改

   <?php
    /*
     * [myRecord 自定义日志记录]
     * @param  [type] $message [description]
     * @param  [type] $level   [description]
     * @param  [type] $tag     [description]
     * @param  [type] $record  [description]
     * @return [type]          [description]
     /
    static function myRecord($type, $message, $level=self::ERR, $tag='', $record=fasle)
    {

        switch($type)
        {

            case 'user':
                {

                    if($record || false !== strpos(C('USR_LOG_LEVEL'),$level))
                    {

                        if(!empty($tag)) self::$userLog[] = "[{ $tag}] [{ $level}]: { $message}\r\n";
                        else self::$userLog[] = "[{ $level}]: { $message}\r\n";
                    }
                    break;
                }
            case 'svr':
                {

                    if($record || false !== strpos(C('SVR_LOG_LEVEL'),$level)) {

                        if(!empty($tag))
                            self::$svrLog[] = "[{ $tag}] [{ $level}]: { $message}\r\n";
                        else
                            self::$svrLog[] = "{ $level}: { $message}\r\n";
                    }
                    break;
                }
            case 'statis':
                {

                    if($record || false !== strpos(C('STATIS_LOG_LEVEL'),$level)) {

                        if(!empty($tag)) self::$statisLog[] = "[{ $tag}] [{ $level}]: { $message}\r\n";
                        else
                            self::$statisLog[] = "[{ $level}]: { $message}\r\n";
                    }
                    break;
                }
        }
    }
    ?>


上述修改主要是把日志信息根据日志来源存储到对应的内存结构中,根据日志级别拼接 级别和tag.日志信息msg在传入的时候就已经拼接了打印日志时所在的代码文件和位置了. 当然打印日志时的代码位置信息为了调用接口的便利性直接从堆栈里取得,这里是否有性能 上的问题还需要做进一步的观察处理 ThinkPHP在请求执行完后会回调Log::save接口,只要在save接口里对上述的内存日志 结构做下判断,有数据就存到不同的日志文件里即可。

业务逻辑在打印日志的时候,只要类似于下面这样调用,就能获得足够的日志信息

   <?php
    SVRLOG_ERR('auction status[online->onsale], auctionId: ' . $auction['auction_id'], 'auction');
    ?>

打印日志结果如下所示:

[ 2017-01-28 21:59:17 ] 180.153.201.79
/yiku/JDOnline/Home/order/getLotOrders?userId%253D587799808d
226 ERR: /common.php:32 | errno[-100] errmsg[input error ]

业务代码层级调整

ThinkPHP中提供了MVC模型.我们的项目主要提供rest api,因此用不到V层。 ThinkPHP中Model主要负责封装db操作,与DB发生交互。ORM的工作也在Model中 做了,操作DB很方便,而Controller主要处理业务逻辑。在项目复杂度,项目规模 不大的情况下可以将所有的业务逻辑都写在Controller里. 但是项目规模大了之后这种方式就不好使了。因此需要独立出来一个Service层专门 处理业务逻辑,Controller 专门负责业务请求转发和简单的数据操作。 这种代码层级规划的好处有以下几点:

  • 流转结构清晰,Controller里获取Service的处理句柄,调用Service里的方法并获取 结果,根据返回结果再决定下一步执行流程,不涉及具体的实现细节。方便开发团队 在Controller里就能获得请求流转的主脉络
  • 提高代码复用度。Service负责实际的数据处理.项目主调除了普通用户,可能还来自 内部管理系统,以及网页端。面对各种端在独立出各自的消息路由层的时候,可以方便 的组合Service的调用来完成接口的调用。将实现独立出来一层而不是与消息路由杂糅在 一起,提高了代码的复用。降低了耦合度

框架上补充了Service的基类,要实现Service的时候只要继承基类即可。 目前基类提供的基础能力是根据子类里配置的过滤字段数组自动过滤敏感字段。 这点在所有接口里几乎都要被用到 下面是Service的基类实现

   <?php
    class Service
    {

        protected $dataFilterOption = array();
        public function dataFilter(&$data){

          if(!empty($this->dataFilterOption)){

            foreach($data as $key=>$value) {

                if(is_array($value)) $this->dataFilter($data[$key]);
                if(!is_numeric($key) && in_array($key, $this->dataFilterOption)) unset($data[$key]);
            }
          }
            return $data;
        }
    };
    ?>


使用的时候只要在子类的dataFilterOption 数组里配置需要过滤的db敏感字段 Controller回包的时候调用dataFilter即可 获取Service处理对象的时候要易于使用,笔者独立了一个Util文件夹,以单例的模式 来获取Service的对象.废话不多说了上代码:

  <?php
    namespace Home\Util;
    use Tink\Log;
    use Home\Services;
    class ServiceUtil{

        /*
         * 取得Service类实例
         * @static
         * @param   $[name] []
         * @access public
         * @return mixed
         /
        static function getInstance($name='') {

                static $instance =  array();
                $service_name =  $name;
                if(!isset($instance[$service_name])){

                    $class = 'Home\Services\'.$service_name;
                    if(!class_exists($class,false)){

                        $obj = new $class();
                        $instance[$service_name] = $obj;
                    }
                    else
                    {

                        \Think\Log::record('service instance not exists'.$service_name,'ERR');
                    }
                }
                return $instance[$service_name];
            }
    };
    ?>

获取Service子类实例的时候只要输入Service类名字就可以获得实例了

 <?php
    $handler = ServiceUtil::getInstance("AuctionService");
    ?>

这种处理方式方便业务逻辑开发的时候获取Service子类实例. 不过这种方式并不具备真正意义上的单例作用。意即减少Service对象的 创建.这跟php本身的生命周期管理有管理。 php作为一门web脚本语言。代码的生命周期只存在于单次的请求中。 php中定义的静态变量在请求结束之后就会被清理回收了。上述的单例模式 仅限于在单次请求中涉及多次对于同一个Service调用的时候减少该Service的 重复初始化。


错误码管理统一

业务处理的时候,对于致命错误的处理采用抛出异常并在主干路径上 捕捉异常的处理方式。示例代码如下:

    <?php
    try {

        $this->init($userId, $auctionId, $lotId, $price, $username);
        $this->beforeBid();
        $this->doBid();
        $this->afterBid();
    }
    catch (Exception $e)
    {

       deal_exception($e);
    }
    ?>

通过上述方式避免了在关键路径上的嵌套调用过深的时候,需要层层返回错误 带来代码维护上的压力。这种抛异常的方式需要记住 抛出的原始位置在哪里。因此笔者重写了Exception的方法。Exception携带 一个常量描述的错误码,该错误码配置在一个专门的错误码文件里。通过 错误码可以在配置里查找到错误描述。Exception还可以携带外带的上下文信息 然后与配置里写死的错误描述拼接在一起返回给客户端并记录日志。 这些工作都在deal_exception里处理了。deal_exception还会获取到抛出异常时的 堆栈信息打印抛出异常的代码文件和位置到日志文件里头,方便快速查找异常抛出位置 错误码统一维护很有必要,避免了团队使用被占用的错误码,需要改错误描述的时候 只需要动配置就行了。通过统一配置的方式提高了代码的内聚性

下面贴下deal_exception的实现

   <?php
    function deal_exception($e){

        $msg = $e->getMessage();
        $trace = $e->getTraceAsString();
        $errno = $e->getErrno();
        Think\Log::record($trace,'ERR');
        reply_error($errno, $msg);
    }
    function reply_error($err, $msg)
    {

        $config_msg = C($err . '.ERRMSG');
        $err_msg = isset($config_msg) ? $config_msg . " " . $msg : $config_msg;
        $data = array();
        $data['errno'] = C($err . '.ERRNO');
        $data['errmsg'] = $err_msg;
        $log = sprintf("errno[%d] errmsg[%s]", $data['errno'], $data['errmsg']);
        SVRLOG_ERR($log);
        echo json_encode($data);
        exit();
    }
    ?>

相关修整代码等项目开发完毕之后再附上链接