YiluPHP
这家伙很懒,什么都没有留下...

经验 强制代码里使用统一定义的常量作为Redis的键名,这样实现

浏览数 128713 最后修改时间
新进一公司,因为历史项目很多,Redis的键名也是随用随写,没有统一的管理,现在需要在一个文件中统一定义,以类常量的形式定义,并且要求程序里一定使用统一定义的常量用为键名,如果没使用就报错。要我写强制限制的代码。搞了半天,终于搞写了最难的一关:检测发起操作的代码是不是使用常量作为键名。
主要的几个技术点:
1、通过debug_backtrace()函数找到操作Redis的起源位置,涉及多级文件的传递调用;
2、使用装饰类监控所有的Redis方法的调用,做到在执行Redis方法之前的检测,这里使用到了类的魔术方法:__call();
3、使用反射原理获取类的全部常量;
4、PHP的sscanf()函数有BUG,自己写了一个匹配字符串和字符串格式的函数;



<?php
/**
 * Redis key统一在这里定义
 */
class REDIS_KEY{
    const REDIS_TEST_KEY = 'REDIS_TEST_KEY_%d_%s_%d';  //在这里备注说明,占位符依次是:文章ID、用户名、页码ID


    /**
     * 获取本类中定义的全部常量
     */
    public static function get_constants() {
        $objClass = new ReflectionClass(__CLASS__);
        return $objClass->getConstants();
    }

    /**
     * 判断本类中是否定义了某个常量
     */
    public static function defined($key) {
        $all = static::get_constants();
        return isset($all[$key]);
    }
}



        //调用方法
        $conf = [...]; //Redis的配置信息
        $type=1; //不同的Redis的配置
        try {
            //原来直接实例化Redis
            // $redis = new Redis();
            //现在实例化Redis的装饰类
            $redis = new redis_decorator($type);
            $redis->connect($conf['host'], $conf['port'], 3);
            $redis->auth($conf['pwd']);
            $instanceRedis[$type] = $redis;
            return $redis;
        } catch (Exception $ex) {
            //写一个错误日志,这个是自定义的错误类,可删除
            write_log($log, __FUNCTION__, null, null, 'redis服务:' . $ex->getMessage() . PHP_EOL . '参数:', 0, 0);
            return false;
        }



<?php
/**
  * Redis类的装饰器
  * 实现在调用Redis方法之前,检测使用的键名是否为统一定义的常量名
  **/
class redis_decorator
{
    private $_type;
//    private $_host;
//    private $_port;
//    private $_timeout;
//    private $_passport;

    public function __construct($type)
    {
        $this->_type = $type;
    }

    public function connect($host, $port, $timeout) {
//        $this->_host = $host;
//        $this->_port = $port;
//        $this->_timeout = $timeout;
    }

    public function auth($passport) {
//        $this->_passport = $passport;
    }

    public function __call($method, $args) {
        //检测是否使用常量调用的
        $trace_list = debug_backtrace();
        //解析出对应的常量名称
        $constant_name = $this->parse_constant_name($args[0]);
        if (false === $constant_name){
            $msg = '请中REDIS_KEY.php类中、以常量的形式定义Redis键名:'.$args[0];
            $this->debug_backtrace($msg, true);
        }
        $use_constant = false;
        $param_pos = 0;
        foreach ($trace_list as $key => $item){
            if ($key==1){
                if (true === $res = $this->direct_use_constant($item)){
                    $use_constant = true;
                    break;
                }
                if ($res===false){
                    break;
                }
                else{
                    $param_pos = $res;
                }
            }
            else if ($key>1) {
                if (true === $res = $this->find_constant_in_caller($item, $param_pos, $constant_name)){
                    $use_constant = true;
                    break;
                }
                if ($res===false){
                    break;
                }
                else{
                    $param_pos = $res;
                }
            }
        }
        if (!$use_constant){
            $msg = '请使用统一定义的redis key常量( REDIS_KEY::'.$constant_name.' )';
            throw new fx_da_exception($msg);
            return;
            $this->debug_backtrace($msg, true);
        }

        static $instanceRedis = array();
        if (!empty($instanceRedis[$this->_type])) {
            $redis = $instanceRedis[$this->_type];
        }
        else {
            if ($this->_type == 1) {
                global $ZP_REDIS;
                $conf = $ZP_REDIS;
                $log = 'redis_error_zp.log';
            } elseif ($this->_type == 2) {
                global $ALI_REDIS;
                $conf = $ALI_REDIS;
                $log = 'redis_error_ali.log';
            } else {
                global $HREDIS;
                $conf = $HREDIS;
                $log = 'redis_error.log';
            }

            try {
                $redis = new Redis();
                $redis->connect($conf['host'], $conf['port'], 3);
                $redis->auth($conf['pwd']);
                $instanceRedis[$this->_type] = $redis;
            } catch (Exception $ex) {
                //写一个错误日志,这个是自定义的错误类,可删除
                write_log($log, __FUNCTION__, null, null, 'redis服务:' . $ex->getMessage() . PHP_EOL . '参数:', 0, 0);
                return false;
            }
        }
        return call_user_func_array([$redis, $method], $args);
    }

    /**
     * 通过redis键名解析出与其对应的常量名
     * @param $key_name
     * @return bool|string
     */
    public function parse_constant_name($key_name)
    {
        $all = REDIS_KEY::get_constants();
        foreach ($all as $key => $value){
            if ($this->sscanf($key_name, $value)){
                return $key;
            }
        }
        return false;
    }

    /**
     * 通过redis键名解析出与其对应的常量名
     * @param $str
     * @param $format
     * @return bool
     */
    public function sscanf($str, $format)
    {
        $index = 0;
        $continue = 0;
        $length = strlen($format);
        $in_placer = false;
        for ($key=0; $key<$length; $key++){
            $value = $format[$key];
            if ($continue>0){
                $continue--;
                continue;
            }
            if ($value=='%' && isset($format[$key+1]) && in_array($format[$key+1], ['b','c','d','e','u','f','F','o','s','x','X'])){
                $in_placer = true;
                $continue = 1; //当次跳出,并且跳过一次
                $index++;
                continue;
            }

            while ($in_placer){
                if ($index>strlen($str) || !isset($format[$key+2])){
                    if ($index>strlen($str) && isset($format[$key+2])){
                        return false;
                    }
                    else if ($index>strlen($str) && !isset($format[$key+2])){
                        return true;
                    }
                    break;
                }
                if ($format[$key]==$str[$index]){
                    $in_placer = false;
                }
                else {
                    $index++;
                }
            }
            if ($continue>0){
                continue;
            }

            if ($value != $str[$index]){
                return false;
            }
            $index++;
        }
        return true;
    }

    /**
     * 在发起函数调用的页面中查找是否使用redis的常量
     * @param $current_trace
     * @param $param_pos
     * @param $constant_name
     * @return bool|int|string
     */
    public function find_constant_in_caller($current_trace, $param_pos, $constant_name)
    {
        $line_code = $this->get_line($current_trace['file'], $current_trace['line']);
        //检查调用函数时是不是直接使用的常量
        preg_match('/REDIS_KEY::'.$constant_name.'/', $line_code, $res);
        if ($res){
            return true;
        }
        //向上查找看是否使用了常量,如果有使用则判断通过
        $line = $current_trace['line']-1;
        if ($line<=0){
            return false;
        }
        $finish = false;
        do {
            $line_code = $this->get_line($current_trace['file'], $line);
            //查找是否有变量的定义
            preg_match('/REDIS_KEY::'.$constant_name.'/', $line_code, $res);
            if ($res){
                return true;
            }
            //是否已经到达函数申明处
            preg_match('/functions+.*?((.*))/', $line_code, $res);
            if ($res){
                $finish = true;
                if (empty($res[1])){
                    return false;
                }
            }

            $line--;
        }
        while($line<=0 || !$finish);
        return false;
    }

    /**
     * 是否上一个页面直接使用的常量
     * @param $current_trace
     * @return bool|int|string
     */
    public function direct_use_constant($current_trace)
    {
        $line_code = $this->get_line($current_trace['file'], $current_trace['line']);
        //检查调用redis时是不是直接使用的常量
        preg_match('/fx_redis_service::.*->w+(s*REDIS_KEY::.*)/', $line_code, $res);
        if ($res){
            return true;
        }
        //获取变量名
        preg_match('/fx_redis_service::.*->w+(s*($[dw_]+).*)/', $line_code, $res);
        if (!$res){
            return false;
        }
        $variable_name = $res[1];
        //向上查找变量的定义处
        $line = $current_trace['line']-1;
        if ($line<=0){
            return false;
        }
        $finish = false;
        do {
            $line_code = $this->get_line($current_trace['file'], $line);
            //查找是否有变量的定义
            preg_match('/\'.$variable_name.'s*=.*REDIS_KEY::([dw_]+).*/', $line_code, $res);
            if ($res){
                $all = REDIS_KEY::get_constants();
                if ( !isset($all[$res[1]]) ){
                    $this->debug_backtrace('Redis键名的常量未定义:'.$res[1].',请在REDIS_KEY.PHP中定义。', true);
                }
                return true;
            }
            //是否已经到达函数申明处
//            $line_code = 'function request_limit()';
            preg_match('/functions+.*?((.*))/', $line_code, $res);
            if ($res){
                $finish = true;
                $params = str_replace(' ', '', $res[1]);
                $params = explode(',', $params);
                //参数中是否有变量
                $has_variable_param = false;
                //变量在函数参数中的位置
                $variable_param_pos = 0;
                foreach ($params as $key => $string){
                    $string = explode('=', $string);
                    if ($string[0]==$variable_name){
                        $variable_param_pos = $key;
                        $has_variable_param = true;
                    }
                }
                if (!$has_variable_param){
                    return false;
                }
                else{
                    //返回变量在函数参数中的位置
                    return $variable_param_pos;
                }
            }

            $line--;
        }
        while($line<=0 || !$finish);
        return false;
    }

    public function get_line($file, $line, $length = 40960)
    {
        static $current_file = null;
        static $handle = null;
        $current_file = $file;

        $returnTxt = null; // 初始化返回
        $i = 1; // 行数

        $handle = @fopen($file, "r");
        if ($handle) {
            while (!feof($handle)) {
                $buffer = fgets($handle, $length);
                if ($line == $i) $returnTxt = $buffer;
                $i++;
            }
            fclose($handle);
        }
        return $returnTxt;
    }

    /**
     * 错误、异常的backtrace
     * @param string $msg
     * @param bool $echo
     * @Time: 2019/11/15   16:54
     */
    public function debug_backtrace($msg, $echo = false)
    {
        $backtrace = debug_backtrace();
        foreach ($backtrace as $i => $l) {
            $msg .= sprintf(
                "<br>	[%d] %s%s%s in %s on line %d%s",
                $i,
                isset($l['class']) ? $l['class'] : '',
                isset($l['type']) ? $l['type'] : '',
                isset($l['function']) ? $l['function'] : '',
                $l['file'],
                $l['line'],
                PHP_EOL
            );
        }
        //写一个错误日志,这个是自定义的错误类,可删除
        write_log('php_error.log', __FUNCTION__, null, null, $msg, 0, 0);

        if ($echo) {
            echo $msg;
            exit;
        }
    }

}


我来说说