强制代码里使用统一定义的常量作为Redis的键名,这样实现
新进一公司,因为历史项目很多,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;
}
}
}