Commit 28daa54f authored by cuiweifeng's avatar cuiweifeng

update :init

parents
composer.lock
/vendor/
.idea
.DS_Store
.project
\ No newline at end of file
生活圈商户系统工具类库,配合yaf框架使用的基础库
\ No newline at end of file
{
"name": "bp/php_utils",
"description": "bp php_utils",
"type": "library",
"require": {
"php": ">=7.2",
"guzzlehttp/guzzle": "6.3",
"catfan/medoo": "1.7.10",
"mongodb/mongodb": "1.4.3",
"ext-mbstring": "*",
"ext-exif": "*",
"ext-fileinfo": "*",
"ext-ctype": "*",
"elasticsearch/elasticsearch": "~7.0"
},
"autoload": {
"psr-4": {
"Api\\PhpUtils\\": "src/"
}
}
}
\ No newline at end of file
<?php
namespace Api\PhpUtils\Cache;
use Yaf\Application;
use Api\PhpUtils\Log\FileLog;
/**
* ApcuUtil 工具类
*
* apcu扩展不支持前缀,在此类中做统一的前缀管理
* 注:
* 1,目前使用apcu4.0,5.0需php7支持(apcu_entry)
* 2,所有实现的方法在原apcu函数基础上添加了prefix参数,并增加appid做为统一的前缀,具体请查看php手册
* 3,因apcu_add及apcu_store等方法批量及单个操作参数完全不同,此处将其分离成两个函数
*
* @date 2021-03-29
*/
class ApcuUtil
{
const PREFIX_NONE = '';
const EXPIRE_NEVER = 0;
const EXPIRE_AFTER_FIVE_SECONDS = 5;
const EXPIRE_AFTER_ONE_MINUTE = 60;
const EXPIRE_AFTER_FIVE_MINUTES = 300;
const EXPIRE_AFTER_HALF_HOUR = 1800;
const EXPIRE_AFTER_ONE_HOUR = 3600;
const EXPIRE_AFTER_SIX_HOURS = 21600;
const EXPIRE_AFTER_ONE_DAY = 86400;
const EXPIRE_AFTER_THREE_DAYS = 259200;
const EXPIRE_AFTER_FIFTEEN_DAYS = 1296000;
const EXPIRE_AFTER_ONE_MONTH = 2592000;
/**
* 为指定key添加前缀
*
* @param [str] $prefix
* @param [mixed] $key
*/
public static function add_prefix($prefix, $key)
{
$appid = Application::app()->getConfig()->get("appid");
$prefix = $appid . ':' . $prefix;
if (is_array($key)) {
foreach ($key as $key_key => $key_value) {
$key[$key_key] = $prefix . $key_value;
}
} else {
$key = $prefix . $key;
}
return $key;
}
/**
* 移除apcu批量获取结果中key的前缀,当单个key时原样返回
*
* @param [str] $prefix
* @param [mixed] $key
* @param [mixed] $apc_result
* @return [mixed]
*/
public static function remove_result_prefix($prefix, $key, $apc_result)
{
$appid = Application::app()->getConfig()->get("appid");
$prefix = $appid . ':' . $prefix;
if (is_array($key)) {
$result = array();
if (! is_array($apc_result)) {
return $apc_result;
}
foreach ($apc_result as $apc_key => $apc_value) {
$result[substr($apc_key, strlen($prefix))] = $apc_value;
}
return $result;
}
return $apc_result;
}
/**
* @link http://php.net/manual/zh/function.apcu-fetch.php
*
* @return mixed The stored variable or array of variables on success; false on failure
*/
public static function apcu_fetch($prefix, $key, &$success = true)
{
$perfix_key = self::add_prefix($prefix, $key);
$apc_result = apcu_fetch($perfix_key, $success);
return self::remove_result_prefix($prefix, $key, $apc_result);
}
/**
* @link http://php.net/manual/zh/function.apcu-store.php
*/
public static function apcu_store_one($prefix, $key, $value, $ttl = 0)
{
$perfix_key = self::add_prefix($prefix, $key);
return apcu_store($perfix_key, $value, $ttl);
}
/**
* @link http://php.net/manual/zh/function.apcu-store.php
*/
public static function apcu_store_multi($prefix, $values, $unused = NULL, $ttl = 0)
{
if (! empty($values)) {
$apc_to_set = array();
foreach ($values as $key => $value) {
$perfix_key = self::add_prefix($prefix, $key);
$apc_to_set[$perfix_key] = $value;
}
return apcu_store($apc_to_set, $unused, $ttl);
}
return false;
}
/**
* @link http://php.net/manual/zh/function.apcu-add.php
*/
public static function apcu_add_one($prefix, $key, $value, $ttl = 0)
{
$perfix_key = self::add_prefix($prefix, $key);
return apcu_add($perfix_key, $value, $ttl);
}
/**
* @link http://php.net/manual/zh/function.apcu-add.php
*/
public static function apcu_add_multi($prefix, $values, $unused = NULL, $ttl = 0)
{
if (! empty($values)) {
$apc_to_add = array();
foreach ($values as $key => $value) {
$perfix_key = self::add_prefix($prefix, $key);
$apc_to_add[$perfix_key] = $value;
}
return apcu_add($apc_to_add, $unused, $ttl);
}
return false;
}
/**
* @link http://php.net/manual/zh/function.apcu-delete.php
*/
public static function apcu_delete($prefix, $key)
{
$perfix_key = self::add_prefix($prefix, $key);
return apcu_delete($perfix_key);
}
/**
* @link http://php.net/manual/zh/function.apcu-exists.php
*/
public static function apcu_exists($prefix, $key)
{
$perfix_key = self::add_prefix($prefix, $key);
if (is_array($key)) {
$apc_result = apcu_exists($perfix_key);
$result = array();
if (empty($apc_result)) {
return $result;
}
return self::remove_result_prefix($prefix, $key, $apc_result);
} else {
return apcu_exists($perfix_key);
}
}
/**
* @link http://php.net/manual/zh/function.apcu-inc.php
*/
public static function apcu_inc($prefix, $key, $step = 1, &$success = true, $ttl = 0)
{
$perfix_key = self::add_prefix($prefix, $key);
return apcu_inc($perfix_key, $step, $success, $ttl);
}
/**
* @link http://php.net/manual/zh/function.apcu-dec.php
*/
public static function apcu_dec($prefix, $key, $step = 1, &$success = true, $ttl = 0)
{
$perfix_key = self::add_prefix($prefix, $key);
return apcu_dec($perfix_key, $step, $success, $ttl);
}
public static function retry_apcu_store_one($prefix, $key, $value, $ttl = 0)
{
$retry = 0;
do {
$res = self::apcu_store_one($prefix, $key, $value, $ttl);
} while (!$res && ++$retry < 3);
return $res;
}
/**
* @param $method mixed(array | object_method) 获取信息的方法
*/
public static function period_get($key, $prefix, $ttl, $lock_prefix, $lock_time, $method)
{
$res = self::apcu_fetch($prefix, $key);
if ($res !== false) {
if (! isset($res['data']) || ! isset($res['expire_at'])) {
FileLog::waring('get bad value from apcu', $key . ' ' . $prefix);
return false;
}
if ($_SERVER['REQUEST_TIME'] >= $res['expire_at']) {
// update
if (self::apcu_add_one($lock_prefix, $key, 1, $lock_time)) {
$data = call_user_func($method, $key);
if ($data !== false) {
$value = array(
'data' => $data,
'expire_at' => $_SERVER['REQUEST_TIME'] + ceil($ttl / 2)
);
self::retry_apcu_store_one($prefix, $key, $value, $ttl);
return $data;
}
}
}
return $res['data'];
} else {
// set
if (self::apcu_add_one($lock_prefix, $key, 1, $lock_time)) {
$data = call_user_func($method, $key);
if ($data !== false) {
$value = array(
'data' => $data,
'expire_at' => $_SERVER['REQUEST_TIME'] + ceil($ttl / 2)
);
self::retry_apcu_store_one($prefix, $key, $value, $ttl);
return $data;
}
}
return false;
}
}
}
<?php
namespace Api\PhpUtils\Cache;
use Api\PhpUtils\Redis\RedisUtil;
use Api\PhpUtils\Log\FileLog;
/**
* CacheUtil 工具类
*
* @date 2021-03-30
*/
class CacheUtil
{
const PREFIX_NONE = '';
const EXPIRE_NEVER = 0;
const EXPIRE_AFTER_FIVE_SECONDS = 5;
const EXPIRE_AFTER_ONE_MINUTE = 60;
const EXPIRE_AFTER_FIVE_MINUTES = 300;
const EXPIRE_AFTER_HALF_HOUR = 1800;
const EXPIRE_AFTER_ONE_HOUR = 3600;
const EXPIRE_AFTER_SIX_HOURS = 21600;
const EXPIRE_AFTER_ONE_DAY = 86400;
const EXPIRE_AFTER_THREE_DAYS = 259200;
const EXPIRE_AFTER_FIFTEEN_DAYS = 1296000;
const EXPIRE_AFTER_ONE_MONTH = 2592000;
private static $instances = [];
private $handler = null;
private $expire = 86400; //@todo 讨论一下,如果不设置有效时间,默认1天,不允许设置长期有效
/**
* 获取实例
*
* @param string $serverName 服务名
* @param array $options 其他参数数组,优先级高于yaconf中的相同配置
* @param string $options['serverRegion'] 服务地区
* @param bool $options['master'] 哨兵模式,默认使用主库,使用从库值为false
* @param string $options['serializer'] 序列化选项,包括 none|php|igbinary, msgpack自己实现msgpack_pack|msgpack_unpack, incr等方法只能使用none,set等方法使用none不能存Array或Object
* @param boolean $master
* @return mixed
*/
public static function getInstance($serverName = '', array $options = [])
{
if (empty($serverName)) {
return null;
}
if (isset(self::$instances[$serverName])) {
return self::$instances[$serverName];
}
return self::$instances[$serverName] = new Self($serverName, $options);
}
private function __construct($serverName = '', array $options = [])
{
if (empty($this->handler)){
//获取连接句柄
$this->handler = RedisUtil::getInstance($serverName, $options);
}
}
public function __call($method, array $args)
{
if (!$this->handler) {
return false;
}
return call_user_func_array([$this->handler, $method], $args);
}
/**
* 获取原始数据
* @param $key
* @return mixed
*/
public function getUnChange($key, $prefix = ''){
return $this->handler->get($this->getCacheKey($prefix, $key));
}
/**
* 保存原始数据
* @param $key
* @param $value
* @param $expire
* @return mixed
*/
public function setUnChange($key , $value , $expire = null, $prefix = ''){
if (is_null($expire)) {
// $expire = $this->expire;
return false;
}
$key = $this->getCacheKey($prefix, $key);
if (is_int($expire) && $expire) {
$result = $this->handler->setex($key, $expire, $value);
} else {
$result = $this->handler->set($key, $value);
}
return $result;
}
/**
* 读取缓存
* @access public
* @param string $key 缓存变量名
* @param mixed $default 默认值
* @return mixed
*/
public function get($key, $prefix = '', $default = false)
{
$value = $this->handler->get($this->getCacheKey($prefix, $key));
if (is_null($value) || false === $value) {
return $default;
}
$jsonData = json_decode($value, true);
// 检测是否为JSON数据 true 返回JSON解析数组, false返回源数据
return (null === $jsonData) ? $value : $jsonData;
}
/**
* 读取多个缓存
* @access public
* @param array $keys 缓存变量名数级
* @param mixed $default 默认值
* @return mixed
*/
public function gets($keys, $prefix = '', $default = false)
{
if(empty($keys) || !is_array($keys)) {
return $default;
}
$keys = array_values($keys);
$values = $this->handler->mGet($this->getCacheKey($prefix, $keys));
if (is_null($values) || false === $values) {
return $default;
}
$result = [];
foreach((array)$values as $i => $value) {
if($value !== false) {
$jsonData = json_decode($value, true);
$result[$keys[$i]] = (null === $jsonData) ? $value : $jsonData;
}
}
return $result;
}
/**
* 写入缓存
* @access public
* @param string $key 缓存变量名
* @param mixed $value 存储数据
* @param integer $expire 有效时间(秒)
* @return boolean
*/
public function set($key, $value, $expire = null, $prefix = '')
{
if (is_null($expire) || empty($expire)) {
// $expire = $this->expire;
return false;
}
$key = $this->getCacheKey($prefix, $key);
//对数组/对象数据进行缓存处理,保证数据完整性
$value = (is_object($value) || is_array($value)) ? json_encode($value) : $value;
if (is_int($expire) && $expire) {
$result = $this->handler->setex($key, $expire, $value);
} else {
$result = $this->handler->set($key, $value);
}
return $result;
}
/**
* 写入多个缓存
* @access public
* @param array $items 缓存数组
* @param integer $expire 有效时间(秒)
* @return boolean
*/
public function sets($items, $expire = null, $prefix = '')
{
if (is_null($expire) || empty($expire)) {
return false;
}
$this->handler->multi();
foreach($items as $key => $value) {
$key = $this->getCacheKey($prefix, $key);
$value = (is_object($value) || is_array($value)) ? json_encode($value) : $value;
$this->handler->setex($key, $expire, $value);
}
return $this->handler->exec();
}
/**
* 新增缓存,只有不存在时才写入成功
* @access public
* @param string $key 缓存变量名
* @param mixed $value 存储数据
* @param integer $expire 有效时间(秒)
* @return boolean
*/
public function add($key, $value, $expire = null, $prefix = '')
{
if (is_null($expire) || empty($expire)) {
// $expire = $this->expire;
}
$key = $this->getCacheKey($prefix, $key);
//对数组/对象数据进行缓存处理,保证数据完整性
$value = (is_object($value) || is_array($value)) ? json_encode($value) : $value;
if(is_int($expire) && $expire) {
$result = $this->handler->setenx($key, $expire, $value);
} else {
$result = $this->handler->setnx($key, $value);
}
return $result;
}
/**
* 自增缓存(针对数值缓存,且不自动序列化)
* @access public
* @param string $key 缓存变量名
* @return false|int
*/
public function incr($key, $prefix = ''){
return $this->handler->incr($this->getCacheKey($prefix, $key));
}
/**
* 自减缓存(针对数值缓存,且不自动序列化)
* @access public
* @param string $key 缓存变量名
* @return false|int
*/
public function decr($key, $prefix = ''){
return $this->handler->decr($this->getCacheKey($prefix, $key));
}
/**
* 自增缓存(针对数值缓存,且不自动序列化)
* @access public
* @param string $key 缓存变量名
* @param int $step 步长
* @return false|int
*/
public function incrby($key, $step = 1, $prefix = '')
{
$key = $this->getCacheKey($prefix, $key);
return $this->handler->incrby($key, $step);
}
/**
* 自减缓存(针对数值缓存,且不自动序列化)
* @access public
* @param string $key 缓存变量名
* @param int $step 步长
* @return false|int
*/
public function decrby($key, $step = 1, $prefix = '')
{
$key = $this->getCacheKey($prefix, $key);
return $this->handler->decrby($key, $step);
}
/**
* 删除缓存
* @access public
* @param string $key 缓存变量名
* @return boolean
*/
public function del($key, $prefix = '')
{
return $this->handler->del($this->getCacheKey($prefix, $key));
}
/**
* 判断缓存
* @access public
* @param string $key 缓存变量名
* @return bool
*/
public function has($key, $prefix = '')
{
return $this->handler->get($this->getCacheKey($prefix, $key)) ? true : false;
}
/**
* 设置有效期
*
* @param string $key
* @param integer $expire
* @param string $prefix
* @return bool
*/
public function expire($key , $expire = 3600, $prefix = ''){
$cacheKey = $this->getCacheKey($prefix, $key);
return $this->handler->expire($cacheKey, $expire);
}
/**
* 返回句柄对象,可执行其它高级方法
*
* @access public
* @return object
*/
public function handler()
{
return $this->handler;
}
/**
* 获取实际的缓存标识
* @access public
* @param string|array $key 缓存名
* @return mixed
*/
public function getCacheKey($prefix, $key)
{
if ($prefix === '') {
return $key;
}
if (is_array($key)) {
$keyArr = [];
foreach ($key as $val) {
$keyArr[] = $prefix . $val;
}
return $keyArr;
}
return $prefix . $key;
}
}
<?php
namespace Api\PhpUtils\Common;
/**
* Class BaseConvertUtil 10进制数和62进制(包含)以内进制数的互转
* 对比PHP自带base_convert函数的测试, 对10000个随机数进行进制转换(10进制到36进制, 36进制到10进制)的耗时(单位s):
* decToOther(10 => 36): 3.4464011192322
* base_convert(10 => 36): 0.3307638168335
* otherToDec(36 => 10): 8.2434189319611
* base_convert(36 => 10): 0.3491358757019
* 从测试结果来看, 36进制内的转换请尽量使用PHP自带base_convert函数
*/
class BaseConvert
{
private static $dict = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
/**
* 十进制数转换成其它进制: 可以转换成2-62任何进制
*
* @param int $num
* @param int $to
* @return string 转换成功返回转换后的字符串, convertStatus转换失败返回false转换失败返回false
*/
public static function decToOther($num, $to = 62, &$convertStatus = true)
{
if ($to == 10)
{
return strval($num);
}
if ($to > 62 || $to < 2)
{
$convertStatus = false;
return '';
}
$ret = '';
do
{
$mod = bcmod($num . '', $to . '');
$ret = self::$dict[intval($mod)] . $ret;
$num = bcdiv($num, $to);
} while ($num > 0);
return $ret;
}
/**
* 其它进制数转换成十进制数: 适用2-62的任何进制
*
* @param string $num
* @param int $from
* @return int|bool 转换成功返回转换后的数值, convertStatus转换失败返回false
*/
public static function otherToDec($num, $from = 62, &$convertStatus = true)
{
if ($from == 10)
{
return $num;
}
if ($from > 62 || $from < 2)
{
$convertStatus = false;
return 0;
}
$num = strval($num);
$len = strlen($num);
$dec = 0;
for ($i = 0; $i < $len; $i++)
{
$pos = strpos(self::$dict, $num[$i]);
if ($pos >= $from)
{
$convertStatus = false;// 如果出现非法字符, 比如16进制中出现w、x、y、z等, 直接返回false
return 0;
}
$dec = bcadd(bcmul(bcpow($from, $len - $i - 1), $pos), $dec);
}
return $dec;
}
}
<?php
namespace Api\PhpUtils\Common;
use Api\PhpUtils\Common\BaseConvert;
class Docid
{
/**
* 校验docid,10位以上docid为自媒体系统post_id fake的docid,U开头
*
* @param string $str
* @return bool
*/
public static function validDocid($str)
{
$strlen = strlen($str);
if ($strlen === 37 && substr($str, 0, 5) === 'news_') return true;
if (0 === strpos($str, 'C-'))//商品的 global id 也是有效的 docid
{
return self::validGlobalid($str);
}
$pos = strpos($str, '_');
if ($pos !== false)
{
$str = substr($str, $pos + 1);
$strlen = $strlen - ($pos + 1);
}
$chklen = 2;
$code = substr($str, -$chklen);
$str = substr($str, 0, $strlen-$chklen);
$strlen -= $chklen;
$base = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
if ($strlen % 2 == 1)
{
$str = '0' . $str;
$strlen += 1;
}
$sum = 0;
$len = $strlen/2;
for ($i=0; $i<$len; $i++)
{
$sum *= 137;
$sum += ord($str[$i]) ^ ord($str[$strlen-$i-1]);
}
$ret = '';
for ($i=$chklen; $i>0; $i--)
{
$idx = $sum % 62;
$sum = ($sum-$idx) / 62;
$ret = $base[$idx] . $ret;
}
return $ret === $code;
}
/**
* docid生成器, [不能用于全局], 仅为了兼容某些文章在未过审时, 没有全局docid, 而客户端又需要文章有docid的情况.
* 用该生成器生成的docid能通过上述validDocid方法的检测
*
* @param int $id
* @param string 生成的docid前缀
* @return string
*/
public static function generateDocid($id, $prefix)
{
$tmp = BaseConvert::decToOther($id);
$len = strlen($tmp);
$len = $len<6?6:$len;
if ($len % 2 == 1)
{
$tmp = '0' . $tmp;
$len += 1;
}
$tmp = str_pad(strval($tmp), $len<6?6:$len, '0', STR_PAD_LEFT);
$sum = 0;
for ($i = 0; $i < $len/2; $i++)
{
$sum *= 137;
$sum += ord($tmp[$i]) ^ ord($tmp[$len - $i - 1]);
}
$code = '';
$base = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
for ($i = 2; $i > 0; $i--)
{
$idx = $sum % 62;
$sum = ($sum - $idx) / 62;
$code = $base[$idx] . $code;
}
return $prefix . $tmp . $code;
}
private static function generateGlobalidCheckString($global_id)
{
$md5_string = md5($global_id);
$crc_32_string = strval(crc32($md5_string));
$pos_array = array("0" => intval(substr($crc_32_string, 1, 1)), "1" => intval(substr($crc_32_string, 3, 1)), "2" => intval(substr($crc_32_string, 5, 1)));
$check_string = substr($md5_string, $pos_array[0], 1) . substr($md5_string, $pos_array[1], 1) . substr($md5_string, $pos_array[2], 1);
return $check_string;
}
private static function validGlobalid($global_id)
{
$str_len = strlen($global_id);
$true_globalid = substr($global_id, 0, ($str_len - 3));
$check_string = self::generateGlobalidCheckString($true_globalid);
if ($check_string !== substr($global_id, -3))
{
return false;
}
return true;
}
}
<?php
namespace Api\PhpUtils\Common;
/**
* geo工具类
*/
class Geo
{
// 地球半径,单位米
const EARTH_RADIUS = 6378137;
/**
* 计算经纬度距离(单位米)
*
* @param float $lat1
* @param float $lng1
* @param float $lat2
* @param float $lng2
* @param bool $km 如果为true则返回单位为km
* @return int
*/
public static function geoDistance($lat1, $lng1, $lat2, $lng2, $km = false)
{
// 转换为弧度
$rad_lat1 = deg2rad($lat1);
$rad_lat2 = deg2rad($lat2);
$rad_lng1 = deg2rad($lng1);
$rad_lng2 = deg2rad($lng2);
$diff_lat = $rad_lat1 - $rad_lat2;
$diff_lng = $rad_lng1 - $rad_lng2;
$distance = 2 * asin(sqrt(pow(sin($diff_lat / 2), 2) + cos($rad_lat1) * cos($rad_lat2) * pow(sin($diff_lng / 2), 2))) * self::EARTH_RADIUS;
if ($km)
{
return round($distance / 1000, 1);
}
return round($distance, 2);
}
}
<?php
namespace Api\PhpUtils\Common;
use Api\PhpUtils\Log\FileLog;
class IP
{
const IPV4 = 1;
const IPV6 = 2;
private static $file = NULL;
private static $fileSize = 0;
private static $nodeCount = 0;
private static $nodeOffset = 0;
private static $meta = [];
private static $v4offset = 0;
private static $v6offsetCache = [];
private static $database = ROOT_PATH . '/data/ipip_china_cn.ipdb';
/**
* 根据ip获取地理位置
*
* @param string $ip
* @return array
*/
public static function find($ip)
{
if (filter_var($ip, FILTER_VALIDATE_IP, [ FILTER_FLAG_IPV4, FILTER_FLAG_IPV6 ])) {
try {
return self::findv4v6($ip);
}catch (\Exception $e) {
FileLog::waring('ipip exception', $e->getMessage());
return [];
}
}
return [];
}
private static function init()
{
if (is_readable(self::$database) === FALSE)
{
throw new \Exception("The IP Database file" . self::$database . "does not exist or is not readable.");
}
self::$file = @fopen(self::$database, 'rb');
if (self::$file === FALSE)
{
throw new \Exception("IP Database File opening " . self::$database . ".");
}
self::$fileSize = @filesize(self::$database);
if (self::$fileSize === FALSE)
{
throw new \Exception("Error determining the size of " . self::$database . ".");
}
$metaLength = unpack('N', fread(self::$file, 4))[1];
$text = fread(self::$file, $metaLength);
self::$meta = json_decode($text, 1);
if (isset(self::$meta['fields']) === FALSE || isset(self::$meta['languages']) === FALSE)
{
throw new \Exception('IP Database metadata error.');
}
$fileSize = 4 + $metaLength + self::$meta['total_size'];
if ($fileSize != self::$fileSize)
{
throw new \Exception('IP Database size error.');
}
self::$nodeCount = self::$meta['node_count'];
self::$nodeOffset = 4 + $metaLength;
}
/**
* @param $ip
* @param string $language
* @return array
* @throws Exception
*/
private static function findv4v6($ip, $language = 'CN')
{
if (self::$file === NULL)
{
self::init();
}
if (is_resource(self::$file) === FALSE)
{
throw new \Exception('IPIP DB closed.');
}
if (isset(self::$meta['languages'][$language]) === FALSE)
{
throw new \Exception("language : {$language} not support.");
}
if (strpos($ip, '.') !== FALSE && !self::supportV4())
{
throw new \Exception("The Database not support IPv4 address.");
}
elseif (strpos($ip, ':') !== FALSE && !self::supportV6())
{
throw new \Exception("The Database not support IPv6 address.");
}
try
{
$node = self::findNode($ip);
if ($node > 0)
{
$data = self::resolve($node);
$values = explode("\t", $data);
$result = array_slice($values, self::$meta['languages'][$language], count(self::$meta['fields']));
if(is_array($result) && !in_array($result[0], ['', '局域网', '本机地址']))
{
return [
'country_name' => $result[0],
'region_name' => $result[1],
'city_name' => $result[2],
'owner_domain' => $result[3],
'isp_domain' => $result[4],
'latitude' => $result[5],
'longitude' => $result[6],
'timezone' => $result[7],
'utc_offset' => $result[8],
'china_admin_code' => $result[9],
'idd_code' => $result[10],
'country_code' => $result[11],
'continent_code' => $result[12],
];
}
}
} catch (\Exception $e)
{
return [];
}
return [];
}
/**
* @param $node
* @return mixed
* @throws Exception
*/
private static function resolve($node)
{
$resolved = $node - self::$nodeCount + self::$nodeCount * 8;
if ($resolved >= self::$fileSize)
{
return NULL;
}
$bytes = self::read(self::$file, $resolved, 2);
$size = unpack('N', str_pad($bytes, 4, "\x00", STR_PAD_LEFT))[1];
$resolved += 2;
return self::read(self::$file, $resolved, $size);
}
private function close()
{
if (is_resource(self::$file) === TRUE)
{
fclose(self::$file);
}
}
/**
* @param $stream
* @param $offset
* @param $length
* @return bool|string
* @throws Exception
*/
private static function read($stream, $offset, $length)
{
if ($length > 0)
{
if (fseek($stream, $offset + self::$nodeOffset) === 0)
{
$value = fread($stream, $length);
if (strlen($value) === $length)
{
return $value;
}
}
throw new \Exception("The Database file read bad data.");
}
return '';
}
private static function supportV6()
{
return (self::$meta['ip_version'] & self::IPV6) === self::IPV6;
}
private static function supportV4()
{
return (self::$meta['ip_version'] & self::IPV4) === self::IPV4;
}
/**
* @param $ip
* @return int
* @throws Exception
*/
private static function findNode($ip)
{
$binary = inet_pton($ip);
$bitCount = strlen($binary) * 8; // 32 | 128
$key = substr($binary, 0, 2);
$node = 0;
$index = 0;
if ($bitCount === 32)
{
if (self::$v4offset === 0)
{
for ($i = 0; $i < 96 && $node < self::$nodeCount; $i++)
{
if ($i >= 80)
{
$idx = 1;
}
else
{
$idx = 0;
}
$node = self::readNode($node, $idx);
if ($node > self::$nodeCount)
{
return 0;
}
}
self::$v4offset = $node;
}
else
{
$node = self::$v4offset;
}
}
else
{
if (isset(self::$v6offsetCache[$key]))
{
$index = 16;
$node = self::$v6offsetCache[$key];
}
}
for ($i = $index; $i < $bitCount; $i++)
{
if ($node >= self::$nodeCount)
{
break;
}
$node = self::readNode($node, 1 & ((0xFF & ord($binary[$i >> 3])) >> 7 - ($i % 8)));
if ($i == 15)
{
self::$v6offsetCache[$key] = $node;
}
}
if ($node === self::$nodeCount)
{
return 0;
}
elseif ($node > self::$nodeCount)
{
return $node;
}
throw new \Exception("find node failed.");
}
/**
* @param $node
* @param $index
* @return mixed
* @throws Exception
*/
private static function readNode($node, $index)
{
return unpack('N', self::read(self::$file, $node * 8 + $index * 4, 4))[1];
}
public function __destruct()
{
self::close();
self::$file = NULL;
}
/**
* 是否国内ip
*
* @param string $ip
* @return bool
*/
public static function inChina($ip = '')
{
// 测试环境支持ip参数
if (empty($ip) && !empty($_GET['test_ip']) && defined('DEVELOPMENT_VERSION_ONLY'))
{
$ip = $_GET['test_ip'];
}
$ip = empty($ip) ? self::ip() : $ip;
if (empty($ip) || self::is_intranet($ip))
{
return true;
}
$location = self::find($ip);
if (isset($location[0]) && $location[0] == '中国')
{
return true;
}
return false;
}
/**
* 获取ip地址
*
* @return void
*/
public static function ip()
{
if (isset($_SERVER['IPV6_REMOTE_ADDR']) && !empty($_SERVER['IPV6_REMOTE_ADDR']) && trim($_SERVER['IPV6_REMOTE_ADDR']) !== '-') // 如果没有ipv6地址, 运维会传一个占位符“-”
{
$ips = $_SERVER['IPV6_REMOTE_ADDR'];
}
else if (!empty($_SERVER['HTTP_CDN_SRC_IP']))
{
$ips = $_SERVER['HTTP_CDN_SRC_IP'];
}
else if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
{
$ips = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
else if (!empty($_SERVER['HTTP_CLIENT_IP']))
{
$ips = $_SERVER['HTTP_CLIENT_IP'];
}
else
{
$ips = $_SERVER['REMOTE_ADDR'];
}
$ip = explode(',', $ips);
return $ip[0];
}
/**
* 获取客户端ip
*
* @return string
*/
public static function remote_ip()
{
$ips = self::user_ips();
return array_pop($ips);
}
public static function user_ips()
{
$ips = '';
if (isset($_SERVER['IPV6_REMOTE_ADDR']) && !empty($_SERVER['IPV6_REMOTE_ADDR']) && trim($_SERVER['IPV6_REMOTE_ADDR']) !== '-') // 如果没有ipv6地址, 运维会传一个占位符“-”
{
$ips .= $_SERVER['IPV6_REMOTE_ADDR'];
}
if (!empty($_SERVER['HTTP_CDN_SRC_IP']))
{
$ips .= ',' . $_SERVER['HTTP_CDN_SRC_IP'];
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
{
$ips .= ',' . $_SERVER['HTTP_X_FORWARDED_FOR'];
}
if (!empty($_SERVER['HTTP_X_REAL_IP']))
{
$ips .= ',' . $_SERVER['HTTP_X_REAL_IP'];
}
$ips .= ',' . $_SERVER['REMOTE_ADDR'];
$internet_ips = array();
$ip = explode(',', $ips);
$ip = array_map(function($item){return trim($item);}, $ip);
foreach ($ip as $one)
{
if (!self::is_intranet($one) && $one !== '')
{
$internet_ips[] = $one;
}
}
return $internet_ips;
}
/**
* 是否是内网地址
*
* @param string $ip
* @return bool
*/
public static function is_intranet($ip)
{
$long = ip2long(trim($ip));
return $long == 2130706433 || //false, 127.0.0.1
($long >= 167772160 && $long <= 184549375) || //10.0.0.0 ~ 10.255.255.255
($long >= 2886729728 && $long <= 2887778303) || //172.15.0.0 ~ 172.31.255.255
($long >= 3232235520 && $long <= 3232301055); //192.168.0.0 ~ 192.168.255.255
}
}
Common工具库
\ No newline at end of file
<?php
namespace Api\PhpUtils\Common;
class Rsa
{
const RSA_ENCRYPT_BLOCK_SIZE = 117;//加密切割长度
const RSA_DECRYPT_BLOCK_SIZE = 128;//解密切割长度
const PRIVATE_KEY_STR = '-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDP2SYzFccMwZxC05Uxwei6ijFcOoJOHPHBK2oRX6ZVDSZMxb7g
hH1HU63abxzcW/+OC845OlxC5XZA9AZtfgEHdYNpEGyaCHE1zu4LsWiovTLpYhV1
Ya9Ks/6ynUecn1P8D3OAKaCuD3DLlawLCRmWlc2EpnwYuJIrEf/OnB7A2QIDAQAB
AoGAEsWa9Jwv6RAHa+WuINtRiJ94i8rg/+sPTpH8N2t7G01fuylU7vQoWGvPVN4a
LjDE6PBaBMMnmAcfYghoGDV8JCWlgxza69JLG6BC/ug4AxKbPVN20okQSkXIRzKQ
2y+nyLk/ud2UJ5revYKv9Off/Byh6cFQJJMXTMB/SQNzJ4ECQQDqwP9T1i2EVMxo
GkYJWyYeKFcUFoHyjH+7cNitlMgd4f4+t7CpkiaXDNRu9nYuUC+T3KNJb/r1kuiQ
HUcZNVeRAkEA4qjGn3p/fmer0YruTX1X8xBIk0bgaKfnTAD8Mo/DvtzCu9L85m4P
y4DMDh9u40/TXeP5kvEh/XovRQedwO0AyQJBAL5WR28hO/yMiMNrchfJ6KkRCjGG
YkxXsIU45OYwuOTJxMvzQfDrSBC23VMuz/mTGFBp15cGjVMpjxiyNGBzCJECQBgA
f2gL9MxR9iPubmXOTC31H3pZGxJ6FUg7InnIN5ZSklyJbzaHmSyXqwQj1/5CScO7
jIY++rZ45eCNeesgLeECQFyzTM+ZcpJmD0gHrR9HOZVRGxuY4VEnlmXskmDofozU
62iHAnRxrSv8I1Fiafok33wS7QkLu3Zq4WqifNsmRwQ=
-----END RSA PRIVATE KEY-----';
const PUBLIC_KEY_STR = '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDP2SYzFccMwZxC05Uxwei6ijFc
OoJOHPHBK2oRX6ZVDSZMxb7ghH1HU63abxzcW/+OC845OlxC5XZA9AZtfgEHdYNp
EGyaCHE1zu4LsWiovTLpYhV1Ya9Ks/6ynUecn1P8D3OAKaCuD3DLlawLCRmWlc2E
pnwYuJIrEf/OnB7A2QIDAQAB
-----END PUBLIC KEY-----';
/**
* VPN项目 SERVER端私钥与客户端公钥
*/
const HUBBLE_PRIVATE_KEY_STR = '-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAL38wjPQ0SqaBWWH
1r6Ot+Cb5wqcHcZDtZ6P3W3BjZM49WOm2RgT71FAZSqpAEFF2ZHFb6/E6Q4GzVYY
sCc36I3HACIBC7HutettRylzYeD6sIc6Cc0Qep1e0qmDIQ7CpqUZafvgVl0DMAzg
HH+6J70H83abZtNahcrFQNkFoEkTAgMBAAECgYAzY64oE+CwFE9HYlHs42/LWoBJ
N2c6XNBAnot3h7ZCz1JY7SWlxKakJLX4rxP5k+pA6tXGBo4/lAlws2bahN3rHDrs
n9Yp/LNHo/w91ewF+lofeDnqE5EELllaEB/q7pHb2QI/OS7NmaR3Ya4oaIMQ6HmS
/fpPhzRO85KhVu+BKQJBAORMBTkTcfMSEc026oi5NHCKUX4pdxxocR+CiMK3LpM4
m33QsuqSx3wHr1cwmextweKxk/DV9QOmHKLSBdT97jcCQQDVCqkyY+0qFEjkJ9hp
CUmRf5bgCBpCwo/X5Ds+kpW0aGnygTyqEAZWKFYNWrfxZ2oTTMgk6GO9ey5q9oBy
qW4FAkEA2hnOO5LaH8+5dgXq1iepc4KRDChtBJS6zdGU+gCq3DcORYpmAzeqoFFv
L6rcwkSxH/NqVIDzDENbxClFLRBSzQJBAIdTtFKcAyT9qae0X9Wo3qCb6Zz5yKSr
rm+3PKLsvkfRI6u1yHFGqJ/GE5N8Eug3hrwnw+Md822Dd+LeHo23bnkCQFUZLyaO
AJs9kaIOS8OCId89fQNTHOXaITxPAF29WZtMZ0UoN9kr4TnqzljHyi4k6RUk+n5N
aavtRtfecEWBSKM=
-----END PRIVATE KEY-----';
const HUBBLE_PUBLIC_KEY_STR = '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCzGJjG6q4T6ddlsL9mflAkJTsZ
KkXgfdyBSiLFLxU+vncB5qEm8LkHi7M8NRcJ7MEaxWO5OyZ1K8YUIS/LI5/PHBP8
e9l6pxBysN4O29xK8YgMr++okxbzEj2BTJqP8RZMAllOS/FtoObcSvIVgJW+zvxG
HrOB+O1JK1HozVV7UwIDAQAB
-----END PUBLIC KEY-----';
/**
* 地铁项目 SERVER端私钥与客户端公钥
*/
const METRO_PRIVATE_KEY_STR = '-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAL8A0U/w8iLhDb/h
thD1PXHPKvovy5p8eym+jMMcE3GSml2MKNYWYfQlAmH7UQCigtpT8lqH47u3/co3
c1PDgXgtOO2FHoKT2T6eoxVbigSupbdvgh/aQUrMULVdkjvWjBnVRSl9+uP4oYhY
wJyFI2+xWtWrG4TEt/zBIl9uMdIjAgMBAAECgYEAoP8Gb3SwFLhQh6GwfoDIpwqx
tirdWr18b0ZfjgI3XW7uUNXYggRh0pHzfTWCJJ/W3kMOMEMEsXE4KeAthLhxgq7g
bJaWYW8n2N8uT/7jmmb1ETOXcExIyKOBSkrH4Tc5sotTR/b0fNt1/wDGaMC6Akwa
rASUqP++tVi1HxOsqdECQQDj60eT1cTvE9YILJ2y254OtSHZ8IcBFULAJGJbGzB+
E5x/z/evwWj6tNhIZQ+5uB0UGEr1BJYitglFpNid52aJAkEA1okvz6KUfmX4jN1J
VYkcyOsOD+jg+0ZlXtpZouIO5RoH7N5TV6tZZNZ5Q9xS4nBH/ym6xGWfnNBiAv5K
Of6ISwJBAKva1HXcUkoTIue2/JHIlz1vxr6WZrWiWlqXFLKtpbrq/q5BGfokVAv2
IL81tM3MzeBBEQP/GVXC/OrOiadGvEECQBf3IUEnn0DzcS2DNsL4Wx/SqjJ6yE8p
kP9kkujQKevL2aDLQTIk8QqDru42uYeUFWbaqYNw+sCQwrIk5/PMjDsCQDhIYzQJ
3yJRBu1qR0MtYe31nGNheNgxFdWPBHb2esVBRjpJHCBgq3yD7F8/fqS2VS4kuZVg
vijw5aJoY41NBh0=
-----END PRIVATE KEY-----';
const METRO_PUBLIC_KEY_STR = '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA9oxWHFsNGFhCrlnNDZkrkhXg
hjo2KfqV5PTG1ytUdqI4UL+gvt0cV0rc3PC1iMk0PBWIM7w0SqqmLp95JH0g6TnU
AIgT6PrMFgXpFVW0MVgn6vtsfi0AAcArkKimbl9o5pQEz36m4OjqTlyhMQKUDVmu
iRaFoFfqBezeUC0gywIDAQAB
-----END PUBLIC KEY-----';
/**
* 商户系统C端 SERVER端私钥与客户端公钥
*/
const MERCHANT_C_PRIVATE_KEY_STR = '-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKp8Y3v5b1cAuAW/
PUEOrUQvNQXSqK/Thcd9a200EQe+TTB5IJQmx4zQXth0pvNnQc8VWdTas+JO0MeN
+jyXuok/HHug8AAA4J6CR3QUazH3A3yf94AaOMAF3iQwww0yiVBBJ1/8dH4X3tTf
QpBHrDcvM0fqZRdEqrJzVbbSaQqFAgMBAAECgYBcZo6E4uw1e25BryTD3fCKxAs4
3LQQl9QqCw/uYAna6IltfDjsExiUeVrudksGITDJzAikFOTqA02k2o6FyLaQSUnS
uRuDGLEGEx6SQVFMH9uZL8E7zjCPmTzvceE0RVyQG5nHkSrmL60enT2+4LnyEhUq
pvUb+Fm+WExyFDXpAQJBAOFM0GE2KLRfdgS/+CmdHfzxj19ytRnNc3/qHez2Ay24
El9gJA7B9y27c2gxYs0RXo7BwIUsaJ2FsgQeI74y5PMCQQDBt3tBsvMPPg21E+Y9
HVFue5sQ0YJ48ygDU3FmvJ66btadq8Qm/cZgiw+/R912C0IrOx+juVpgLzds1UBH
H5CnAkAs8DVgLn1P/Pp9WA0Xv88LdOM0Fp0o2GIBUaFGcouPwvdnLrYCe43iAVGz
pqYeYSiLMPpFleYIBXLMf3aBqYUhAkEAutUj47GcC09mGQcgQEFOkKhWXloy5ERg
ixSngtbYoCbWhytq+9A2zFZS0cGWLoudPeTAF0Blke74HddvLymacQJBALrbUyct
DOPupkDtGl3vN1fAefazMgcn5IfQfVvX6yCu4A7n+qr6G5cHAMlMeEAqdFCLmF+f
NM+VtU//T1kQpvE=
-----END PRIVATE KEY-----';
const MERCHANT_C_PUBLIC_KEY_STR = '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDatQ6W6EjzO2HsfSmNeFQt7iAT
FHYZ0W5URuWPyPAvKnsMJz/vPpYVi1C1cvE/Sguz24qA6819Jr+HyhMADiyCohia
l/zmTGPztP0rXGEHbU5MuQAHrFcDcOwgWcjtHggOV/XxNbmAJf/AwToSfQKkmsJ2
PE1lYJXINxgETj88uwIDAQAB
-----END PUBLIC KEY-----';
/**
* 商户系统C端-测试 SERVER端私钥与客户端公钥
*/
const TEST_MERCHANT_C_PRIVATE_KEY_STR = '-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAMX9eY3yNnhcRt0I
f//WJvVztaqT7GTkwDDxSeUgtbyXGjOKz9Mgahkk5lWyAlLqKXUpd+5Fksue5YWU
LL6OogoiTBNNNsPWoc9l0Wj0KKVdK3ZjnICZEz1LmjiLa++rAB/7fMYUbs6cRtK8
Nl8n9a6cjReYaW9r7tb/pSdC+T0HAgMBAAECgYEAmxEwn/UhplkXg0yB+909fCaZ
FTVwA4GJ6YtBxfw9eqyZ7957cS9FCdZ1tI6L9WbC+6lz7iF9nI8h0uI/vSu3Snm/
MFYGkcsolDBFYsTPru5GouHh4bW6X5Ge6fmz7JgGaqzOtsbD5oh7zN9SsUUb0jqM
9zR5qbw5i8Krut66zGECQQDo8CmWy5jtSJ7wUGBVECHX4dPO2lwyAC13u4Pat8dg
8YA6VzdzzHdhlWLaGuPICKPjBwxVxP6qKT8bPiAYeIgXAkEA2ZeNYQ8lN5v+hosR
dU3FVwva3d7OmIT7unEMk2Nav1YqUHLektsAYZQW1kOtfUvmtWXlba34a7E8FRow
Wt4YkQJBAJYF/7MoQ8EnUX5TNgfzEOj6gDEsf86QQLhpo91dGeM9ByoL/CHvoT/P
N9BjGys9LVhNtMsUyjQLxqIkHAXGs1cCQQDF4+olkpOjfpC+Ji5Lg95sqxBzZlZ4
KcGSr4vjY6G9kAzNu5qm7tvq4rbeSRC4elYpNbGxzV23+6mL+5Rsn76xAkBmABwk
XXLMgdt6NG4x3cM4WeMPbJPWHOXRLRRMukDGW+HklFkpHFbuGSXv+FkjCWhnE9QX
98wubX4hRtsNX8UH
-----END PRIVATE KEY-----';
const TEST_MERCHANT_C_PUBLIC_KEY_STR = '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKTlW0/1dpGK2FeUHvOf1Vus7g
yy4Yx6r0JTtZUfC0u7/Tr/2DaD4xt+OLOW/mY158azfHJO1yBIhSyxi9sKILmiV5
fpbAOKUeRJelNa6tXC+1E8dooh+s1NmZaGXn2F6HK0OtU5Qc/Ebqunm+re8Ek2lN
2ezqViipdMY2tosl2QIDAQAB
-----END PUBLIC KEY-----';
/**
* 商户系统B端 SERVER端私钥与客户端公钥
*/
const MERCHANT_B_PRIVATE_KEY_STR = '-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKtuzhX9pEdN2Dth
XMJCEvSsn7sohoPgtzD9jB9pdkhyoQDqUL8NkPPn6qeFQUr/SzxloqXcxpsP3iWJ
yJNCdRIpDh9YUQWEG8qn9pncVEtzZgGt1wwOXrkxwgzS2tNmtgvOO/nqbZouAWoE
pEkZDZmexAH2qqKb+swz3KSH41UBAgMBAAECgYAqFfzAB1HCM4knIsLb6c6vhIV+
M/e5/nvaqhQxTfm81W9TQzLphFIWntPLpnoeCEiXHjhs0aNsQ9v8iTPVexY0JSpn
5unyhJKo5mENNFjplWBhQKRbTH3YZlODeSCPm+D0+PdWmOXOP6Ytz0UBhdPRN/yH
eab3IGxlHpUYWTO1pQJBANtLNIYgAQhW1OWVPnw+K8K4HBqBefcgvE2gXW410l2e
b+LPKHNJdSD2jfQ3vL1S30dNF4Le6xHZ9gO/RZbMiLcCQQDIIL8LIAyK1bD9PNjA
ZZY9+VK6XxId+Gyuu+cKlb/GhoO4L2QR9B8b1VN5B3MwYY8lI/Y9Zz1JDtD0E8b/
iSgHAkAmgBOeoKvNvQDvtb4crgWc0BoPBUBWhObKcic1HDnsuc5G8bvWUcVAnkjZ
4Zc/XfdZW02Pane20J+ClOgwy4QNAkBc29zpQ5iuPtMd3cLRGxHodaxuL7jPUTrU
pPliTvOqITYzG0I6lmRs3MIbkVpgY5Qw7ovOF/42zPollpuqbDC3AkEAqY4lGPbT
g1uiYbiGVbBNMdWIMDU1igfgA5kcSpV/H32HBbDBFRMeQRj8sA1Odr2x1NoQ4kmr
v7eca+YO5jyZoQ==
-----END PRIVATE KEY-----';
const MERCHANT_B_PUBLIC_KEY_STR = '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2RdTxS2mxmFQUDSeBvUr4wTTX
ugBJw71/jXXo1l5mB4SOhP0fW7UwZSb3myfCMFJZQnCFdxY8LCFvEZYz4f/o8wVA
mDIZoqwIClqMBt2uJvAa4eVwn1Zr1GOnaYs+X8wakZUyIYvQgn98tr2EwN11u4ek
GeBFb3ObYV22i+jziwIDAQAB
-----END PUBLIC KEY-----';
/**
* 商户系统B端-测试 SERVER端私钥与客户端公钥
*/
const TEST_MERCHANT_B_PRIVATE_KEY_STR = '-----BEGIN PRIVATE KEY-----
MIICeQIBADANBgkqhkiG9w0BAQEFAASCAmMwggJfAgEAAoGBAKQGB1lqs3dCTGGj
qONSu5gMBucejojaF23073kIBdwfzZYSxZWw0P+YaXU6JkE8JCaaakNhTNuJmntG
3RBBJe1xaeWZo4Jca2SGy0cR0uMM5mAMtBL2AZ/1RU+D2CKbBl9H0uxB4FF+td2g
Q/C/spKDX4CDQGe1wZxr3Cyh11/JAgMBAAECgYEAoO7BWzxT5dmI8+wOEyzjnuV+
CFp041WI03wVUp1KkpYpsuusLZkVFxNFtZ1eV3dYyP+M75uSO30hs37t+7rQKDyD
9H+WzJCqe5KR5J7xvs5DWuXLCzgNYq8KdXYk+Zy8RuwB9eaWcy1F/i2dYxoz3JUw
Xsmu36Mp994bJJsLKAECQQDRA8quM9YzMXoBd/QTTw5pzKFlrNl+7eF2SMNOdCZB
IVjfOh3/d39A1I46HsvMfd1L9DMvcgz8J3/RK0/iNVWBAkEAyOUeJXH/NhbfXHr4
633DpS+jBKywCYkdhqJEh0Q3dRg/qk6q+9I2f2wS1JPmGq4FMeOxrXjpjAK97Djb
k8z+SQJBAM10obXNZJ/b4acx521Xh75+FKSiXQVFBhxEeB9VUwkwo3CCa1xkdWUf
y1LcVM6FZpOzzuCzhooStbTBk3uzm4ECQQCJWMuhgiyfyEdxLMneBp+p4cWsPnwk
KNA86de8OxFHRwrP3vTbMb78ouDE4VDiULNQhoFFPnM9u+opBD5q8C2xAkEAv2W3
fw+LtVaDnpfWWCN5y6W4gzRtfdSdSvsZpS+EY/RG0cpmN+WR+86ft3WdIIr2Bnm4
RcPnfPSsz3dgAaCR2A==
-----END PRIVATE KEY-----';
const TEST_MERCHANT_B_PUBLIC_KEY_STR = '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+2roRCWyvIl04NtZnNxyYPLAP
nM+7fL+6wGhhc3ilqk4zgr+hxdftUJ30NDkTqjbWe4wvd4ax1xVAS4la5PAK6g5W
BEFar4gu3JgkmyfT3NtwXCd7kZuH98k/7oSVQbxYc9ydwoqwDdXoIVOKtK5YWkoL
+WjPxg37zWCi3Wq5dQIDAQAB
-----END PUBLIC KEY-----';
/**
* TEST SERVER端私钥与客户端公钥
*/
const TEST_PRIVATE_KEY_STR = '-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAPUJqTt1IsuidXzF
qgImn0bYxuNgC4LGku6NMS8VkYeypnGbRqwyQVMcJKYqMqoacgtW3ImiahAfadUT
suxA6YaWRploRCshZlpqncCjS5TxC/95vwsz7Wm16vMft+gYrw2pqwMjLcP2y5bG
ppXUMtKs1d26VSisiBcPLNdOzEZVAgMBAAECgYAqqjL4Mk4LrM7OGdCfLUBgYpBG
uMtoStD8jkL5EsVTHk0SzNmCVAHByc2rzP8jVRV0WfxuToQvKHkR66skray7NijW
2hxkte/mBvGKGbLRvGo08EGFDTP+REdR9s322k6BCg7KH7aXJUlcpWmEbBGuWlXt
L/0pMQJBYH+kcIEJIQJBAPvgA6nZAPyMCMeIEUYQmiRy66VWN8LxYsk3kP1MqyCY
uKE8YDhAwV256r7362VXiouZ2kYG57ZnTqOjYX+vS8kCQQD5DPs3VcQgfKIc9Waa
kW62hhFIxFRPsNDrLzdfGxZgLN7+Ej9AnAwXKXQ9kPdZBoanhWKOWCSwrutPrEHq
LlQtAkAD9l84YdCRKIbO1MYoywQRxLkOTc/swi1iqvwd8AHVLnHhH3kICYTBO7aE
sJPEcV8dmRsE9QDaLr/e9SgIgTJ5AkEAsIo7KaZqOH0gjVaHPNoKmOfAKMzC1xqH
fa8imRAidFpaV8+ndq0nBOQlud6g90JhAS7UBgMFPxk/RGLe9pmQoQJARGiilRNq
wQKlGXa9UTZANZkSVdPOSLfFsfqdvEkKCNHA4S3yIM1lDFB0MiooT8RsFJ2vsShr
6QnQQiRBFV4M0Q==
-----END PRIVATE KEY-----';
const TEST_PUBLIC_KEY_STR = '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC203n7eo7f4eIs9P6RUIxuKQZP
WfnAeW2OpRs5AnxtUBBLKLdKtuEuZclb7Nqt/tncSuAb63p4Zt/a7JQ7nPE24dMk
mnRjVUtXBgEF0A9xt2QVNMQovtjJ2rkg43gVByDKbOsUqbJYjA12IpALMdECHCMl
817QbgfnBbuIvzbImQIDAQAB
-----END PUBLIC KEY-----';
private static $switchApp = [
'yidian' => [
'pub_key' => self::PUBLIC_KEY_STR,
'pri_key' => self::PRIVATE_KEY_STR,
],
'hubble' => [
'pub_key' => self::HUBBLE_PUBLIC_KEY_STR,
'pri_key' => self::HUBBLE_PRIVATE_KEY_STR,
],
'metro' => [
'pub_key' => self::METRO_PUBLIC_KEY_STR,
'pri_key' => self::METRO_PRIVATE_KEY_STR,
],
'merchant-c' => [
'pub_key' => self::MERCHANT_C_PUBLIC_KEY_STR,
'pri_key' => self::MERCHANT_C_PRIVATE_KEY_STR,
],
'test-merchant-c' => [
'pub_key' => self::TEST_MERCHANT_C_PUBLIC_KEY_STR,
'pri_key' => self::TEST_MERCHANT_C_PRIVATE_KEY_STR,
],
'merchant-b' => [
'pub_key' => self::MERCHANT_B_PUBLIC_KEY_STR,
'pri_key' => self::MERCHANT_B_PRIVATE_KEY_STR,
],
'test-merchant-b' => [
'pub_key' => self::TEST_MERCHANT_B_PUBLIC_KEY_STR,
'pri_key' => self::TEST_MERCHANT_B_PRIVATE_KEY_STR,
],
'test' => [
'pub_key' => self::TEST_PUBLIC_KEY_STR,
'pri_key' => self::TEST_PRIVATE_KEY_STR,
],
];
/**
* RSA 签名
*
* @param string $data
*/
public static function sign_with_private($data, $app = 'yidian')
{
$encrypted = '';
if(isset(self::$switchApp[$app]['pri_key'])) {
$pri_key = openssl_pkey_get_private(self::$switchApp[$app]['pri_key']);
openssl_sign($data, $encrypted, $pri_key, OPENSSL_ALGO_SHA256);
$encrypted = base64_encode($encrypted);
}
return $encrypted;
}
/**
* rsa 验证签名
*
* @param string $signed_string
* @param string $sign_string
* @param bool
*/
public static function verify_with_public($signed_string, $sign_string, $app = 'yidian')
{
$ok = false;
if(isset(self::$switchApp[$app]['pub_key'])) {
$pub_key = openssl_pkey_get_public(self::$switchApp[$app]['pub_key']);
$signed_string = base64_decode($signed_string);
$ok = (bool)openssl_verify($sign_string, $signed_string, $pub_key, OPENSSL_ALGO_SHA256);
}
return $ok;
}
/**
* 私钥加密
*
* @param string $data 加密字符串
* @return string
*/
public static function encode_with_private($data, $app = 'yidian')
{
$encrypted = '';
if(isset(self::$switchApp[$app]['pri_key'])) {
$pri_key = openssl_pkey_get_private(self::$switchApp[$app]['pri_key']);
$data = str_split($data, self::RSA_ENCRYPT_BLOCK_SIZE);
foreach ($data as $block) {
openssl_private_encrypt($block, $dataEncrypt, $pri_key);
$encrypted .= $dataEncrypt;
}
}
return self::urlsafe_b64encode($encrypted);
}
/**
* 公钥加密
*
* @param string $data 加密字符串
* @return string
*/
public static function encode_with_public($data, $app = 'yidian')
{
$encrypted = '';
if(isset(self::$switchApp[$app]['pub_key'])) {
$pub_key = openssl_pkey_get_public(self::$switchApp[$app]['pub_key']);
$data = str_split($data, self::RSA_ENCRYPT_BLOCK_SIZE);
foreach ($data as $block) {
openssl_public_encrypt($block, $dataEncrypt, $pub_key);
$encrypted .= $dataEncrypt;
}
}
return self::urlsafe_b64encode($encrypted);
}
/**
* 私钥解密
*
* @param $private_key私钥
* @param string $encrypted解密字符串
* @return string
*/
public static function decode_with_private($encrypted, $app = 'yidian')
{
$decrypted = '';
if(isset(self::$switchApp[$app]['pri_key'])) {
$pri_key = openssl_pkey_get_private(self::$switchApp[$app]['pri_key']);
$data = str_split(self::urlsafe_b64decode($encrypted), self::RSA_DECRYPT_BLOCK_SIZE);
foreach ($data as $block) {
openssl_private_decrypt($block, $dataDecrypt, $pri_key);
$decrypted .= $dataDecrypt;
}
}
return $decrypted;
}
/**
* 公钥解密
*
* @param string $encrypted解密字符串
* @return string
*/
public static function decode_with_public($encrypted, $app = 'yidian')
{
$decrypted = '';
if(isset(self::$switchApp[$app]['pub_key'])) {
$pub_key = openssl_pkey_get_public(self::$switchApp[$app]['pub_key']);
$data = str_split(self::urlsafe_b64decode($encrypted), self::RSA_DECRYPT_BLOCK_SIZE);
foreach ($data as $block) {
openssl_public_decrypt($block, $dataDecrypt, $pub_key);
$decrypted .= $dataDecrypt;
}
}
return $decrypted;
}
public static function urlsafe_b64encode($string)
{
$data = base64_encode($string);
$data = str_replace(array('+','/','='),array('-','_',''),$data);
return $data;
}
public static function urlsafe_b64decode($string)
{
$data = str_replace(array('-','_'),array('+','/'),$string);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
return base64_decode($data);
}
}
<?php
namespace Api\PhpUtils\Common;
use Api\PhpUtils\Http\Request;
class TimeOut
{
private $timeout_95;
private $timeout_99;
private $retry_95;
private $retry_99;
/**
* TimeOut constructor.
* @param int $timeout_95 毫秒
* @param int $timeout_99 毫秒
* @param int $retry_95 重试次数
* @param int $retry_99 重试次数
*/
public function __construct($timeout_95 = 300, $timeout_99 = 800, $retry_95 = 1, $retry_99 = 2)
{
$this->timeout_95 = $timeout_95;
$this->timeout_99 = $timeout_99;
$this->retry_95 = $retry_95;
$this->retry_99 = $retry_99;
}
/**
* GET请求
* @param $url
* @param array $params
* @param array $headers
* @return array|bool|mixed
*/
public function runGet($url, $params = [], $headers = []){
$res = (new Request())->get($url, $params, $this->timeout_95, $headers, $this->retry_95);
if($res["code"] != 999999){
return $res['response'];
}
$res = (new Request())->get($url, $params, $this->timeout_99, $headers, $this->retry_99);
if($res["code"] != 999999){
return $res['response'];
}
return false;
}
public function runPost($url, $params = [], $content_type = null, $headers = []){
$res = (new Request())->post($url, $params, $this->timeout_95, $content_type, $headers, $this->retry_95);
if($res["code"] != 999999){
return $res['response'];
}
$res = (new Request())->post($url, $params, $this->timeout_99, $content_type, $headers, $this->retry_99);
if($res["code"] != 999999){
return $res['response'];
}
return false;
}
}
\ No newline at end of file
<?php
namespace Api\PhpUtils\Elastic;
use Api\PhpUtils\Elastic\Manager\DocumentManager;
use Api\PhpUtils\Elastic\Manager\IndexManager;
use Elasticsearch\ClientBuilder;
final class ElasticUtil
{
use IndexManager, DocumentManager;
/**
* @var ElasticUtil
*/
private static $instance;
/**
* ElasticUtil constructor.
*/
public function __construct()
{
$hosts = ['http://localhost:9200']; //todo 读取yaconf
$this->client = ClientBuilder::create()->setHosts($hosts)->build();
}
/**
* @return ElasticUtil
*/
public static function getInstance(): ElasticUtil
{
if (empty(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
}
\ No newline at end of file
<?php
namespace Api\PhpUtils\Elastic\Manager;
use Elasticsearch\Client;
trait DocumentManager
{
/**
* @var Client
*/
protected $client;
/**
* 索引文档
* @param $index
* @param $data
* @param string $id
* @return array|callable
*/
public function indexDoc($index, $data, string $id = '')
{
$params = [
'index' => $index,
'body' => $data
];
if (!empty($id)) {
$params['id'] = $id;
}
return $this->client->index($params);
}
/**
* @param $data
* @return array|callable
*/
public function bulkIndexDoc($data)
{
$params = ['body' => $data];
return $this->client->bulk($params);
}
/**
* 删除文档
* @param $index
* @param $id
* @return array|callable
*/
public function deleteDoc($index, $id)
{
$params = [
'index' => $index,
'id' => $id
];
return $this->client->delete($params);
}
/**
* 获取文档
* @param $index
* @param $id
* @return array|callable
*/
public function getDoc($index, $id)
{
$params = [
'index' => $index,
'id' => $id
];
return $this->client->get($params);
}
/**
* 更新文档
* @param $index
* @param $id
* @param $data
* @return array|callable
*/
public function updateDoc($index, $id, $data)
{
$params = [
'index' => $index,
'id' => $id,
'body' => $data
];
return $this->client->update($params);
}
/**
* 搜索操作
* @param $index
* @param $data
* @return array|callable
*/
public function searchDoc($index, $data)
{
$params = [
'index' => $index,
'body' => $data
];
return $this->client->search($params);
}
}
\ No newline at end of file
<?php
namespace Api\PhpUtils\Elastic\Manager;
use Elasticsearch\Client;
trait IndexManager
{
/**
* @var Client
*/
protected $client;
/**
* 创建一个索引
* @param $index
* @param $data
* @return array
*/
public function createIndex($index, $data): array
{
$params = [
'index' => $index,
'body' => $data
];
return $this->client->indices()->create($params);
}
/**
* 删除一个索引
* @param $index
* @return array
*/
public function deleteIndex($index): array
{
$params = [
'index' => $index,
];
return $this->client->indices()->delete($params);
}
/**
* 获取一个或多个索引的当前配置参数
* @param array $indices
* @return array
*/
public function getSettings(array $indices): array
{
$params = [
'index' => $indices
];
return $this->client->indices()->getSettings($params);
}
/**
* 更改索引的配置参数
* @param $index
* @param $data
* @return array
*/
public function putSettings($index, $data): array
{
$params = [
'index' => $index,
'body' => $data
];
return $this->client->indices()->putSettings($params);
}
/**
* 获取索引和类型的映射细节
* @param $index
* @return array
*/
public function getMapping($index): array
{
$params = [
'index' => $index,
];
return $this->client->indices()->getMapping($params);
}
/**
* 更改或增加一个索引的映射
* @param $index
* @param $data
* @return array
*/
public function putMapping($index, $data): array
{
$params = [
'index' => $index,
'body' => $data
];
return $this->client->indices()->putMapping($params);
}
}
\ No newline at end of file
<?php
namespace Api\PhpUtils\Http;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\Handler\CurlMultiHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\RetryMiddleware;
use Api\PhpUtils\Log\FileLog;
class Base
{
/**
* 最大重试次数
*/
private $retry = 0;
/**
* 配置(参照https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html)
*/
private $config = [
'allow_redirects' => true, //允许302跳转
'verify' => false, //禁用证书验证
'connect_timeout' => 3, //连接超时毫秒数
'timeout' => 3, //请求超时毫秒数
];
private $log = [
'url' => '',
'params' => [],
'response' => ''
];
public function __construct()
{
}
public function initClient($custom_config = [], $url = [], $params = [])
{
$this->log['url'] = $url;
$this->log['params'] = $params;
//
if (isset($custom_config['retry']) && !empty($custom_config['retry'])) {
$this->retry = intval($custom_config['retry']);
}
//创建Handler
if (isset($custom_config['concurrent']) && !empty($custom_config['concurrent'])) {
$handlerStack = HandlerStack::create(new CurlMultiHandler());
} else {
$handlerStack = HandlerStack::create(new CurlHandler());
}
//创建重试中间件,指定决策者为 $this->retryDecider(),指定重试延迟为 $this->retryDelay()
$handlerStack->push(Middleware::retry($this->retryDecider(), $this->retryDelay()));
//指定handler
$this->config['handler'] = $handlerStack;
//使用自定义timeout覆盖默认timeout
if (is_array($custom_config) && !empty($custom_config)) {
$this->config = array_merge($this->config, $custom_config);
}
return new Client($this->config);
}
/**
* 确定重试逻辑:false表示不重试,否则重试
* @return
*/
protected function retryDecider()
{
return function (
$retries,
Request $request,
Response $response = null,
RequestException $exception = null
) {
// 超过最大重试次数,不再重试
if ($retries >= $this->retry) {
return false;
}
// 请求失败,继续重试
if ($exception instanceof ConnectException) {
$this->log['response'] = $exception->getMessage();
$this->addLog();
return true;
}
if (!empty($response)) {
// 如果请求有响应,但是状态码大于等于500,继续重试
if ($response->getStatusCode() >= 500) {
$this->log['response'] = 'getStatusCode = ' . $response->getStatusCode();
$this->addLog();
return true;
}
}
return false;
};
}
/**
* 返回一个匿名函数,该匿名函数拉开重试间隔(毫秒)
* @return
*/
protected function retryDelay()
{
return function ($numberOfRetries) {
return RetryMiddleware::exponentialDelay($numberOfRetries) * 100;
};
}
private function addLog(){
FileLog::info('retry', json_encode($this->log));
}
}
Http工具库
\ No newline at end of file
<?php
namespace Api\PhpUtils\Http;
use Api\PhpUtils\Log\FileLog;
use Api\PhpUtils\Mon\MonUtil;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\TransferStats;
class Request
{
private $client = null;
//最大的并发数量
private $concurrency_num = 5;
private $success_code = 0;
private $fail_code = 999999;
private $result = [];
protected $con_result = [];
private $log = [
'url' => [],
'params' => [],
'response' => []
];
public function __construct()
{
}
/**
* @param string $url 请求url
* @param array $params 请求参数
* @param int $timeout 自定义超时毫秒数
* @param array $headers 自定义headers
* @param int $retry 重试次数(默认不重试)
* @param bool $use_mon 是否使用监控打点(默认是)
* @param string $proxy 代理(比如http://192.11.222.124:8000)
* @return array
* @throws
*/
public function get($url, $params = [], $timeout = 0, $headers = [], $retry = 0, $use_mon = true, $proxy = '')
{
try {
//验参
if (empty($url) || !is_array($params)) {
throw new \Exception('params error');
}
$this->log['url'] = $url;
$this->log['params'] = $params;
//构造client
$config = [];
if (!empty($retry) && is_int($retry)) {
$config['retry'] = $retry;
}
$this->client = (new Base())->initClient($config, $url, $params);
//构造配置
if (!empty($params)) {
$options['query'] = $params;
}
if (!empty($timeout)) {
$options['timeout'] = $timeout / 1000;
}
if (!empty($headers)) {
$options['headers'] = $headers;
}
if (!empty($proxy)) {
$options['proxy'] = $proxy;
}
$options['on_stats'] = function (TransferStats $stats) use ($use_mon, $url) {
$this->result['http_code'] = $stats->getHandlerStat('http_code');
if (!empty($use_mon)) {
//MonUtil::proxyMon($url, $stats->getHandlerStat('http_code'), round($stats->getHandlerStat('total_time'),4) * 1000);
}
};
//异步get请求
$promise = $this->client->getAsync($url, $options);
$promise->then(
function (Response $resp) {
$ret = $this->log['response'] = $resp->getBody()->getContents();
$ret = empty(json_decode($ret, true)) ? $ret : json_decode($ret, true);
$this->result['code'] = $this->success_code;
$this->result['response'] = $ret;
},
function (RequestException $e) {
$this->log['response'] = $e->getMessage();
throw new \Exception($e->getMessage());
}
);
$promise->wait();
} catch (\Exception $e) {
$this->result['code'] = $this->fail_code;
$this->result['msg'] = $e->getMessage();
} finally {
$this->addLog('get');
return $this->result;
}
}
/**
* @param $url
* @param array $params 请求参数
* @param int $timeout 自定义超时毫秒数
* @param string $content_type 默认form_params|json|multipart(文件上传)
* @param array $headers 自定义headers
* @param int $retry 重试次数
* @param bool $use_mon 是否使用监控打点(默认是)
* @param string $proxy 代理(比如http://192.11.222.124:8000)
* @return array
* @throws
*/
public function post($url, $params = [], $timeout = 0, $content_type = '', $headers = [], $retry = 0, $use_mon = true, $proxy = '')
{
try {
//验参
if (empty($url) || !is_array($params)) {
throw new \Exception('params error');
}
$this->log['url'] = $url;
$this->log['params'] = $params;
//构造client
$config = [];
if (!empty($retry) && is_int($retry)) {
$config['retry'] = $retry;
}
$this->client = (new Base())->initClient($config, $url, $params);
//构造配置
if (!empty($params)) {
if ($content_type == 'json') {
$options['json'] = $params;
} elseif ($content_type == 'multipart') {
$options['multipart'] = $params;
} else {
$options['form_params'] = $params;
}
}
if (!empty($timeout)) {
$options['timeout'] = $timeout / 1000;
}
if (!empty($headers)) {
$options['headers'] = $headers;
}
if (!empty($proxy)) {
$options['proxy'] = $proxy;
}
$options['on_stats'] = function (TransferStats $stats) use ($use_mon, $url) {
$this->result['http_code'] = $stats->getHandlerStat('http_code');
if (!empty($use_mon)) {
//MonUtil::proxyMon($url, $stats->getHandlerStat('http_code'), round($stats->getHandlerStat('total_time'),4) * 1000);
}
};
//异步post请求
$promise = $this->client->postAsync($url, $options);
$promise->then(
function (Response $resp) {
$ret = $this->log['response'] = $resp->getBody()->getContents();
$ret = empty(json_decode($ret, true)) ? $ret : json_decode($ret, true);
$this->result['code'] = $this->success_code;
$this->result['response'] = $ret;
},
function (RequestException $e) {
$this->log['response'] = $e->getMessage();
throw new \Exception($e->getMessage());
}
);
$promise->wait();
} catch (\Exception $e) {
$this->result['code'] = $this->fail_code;
$this->result['msg'] = $e->getMessage();
} finally {
$this->addLog('post');
return $this->result;
}
}
/**
* 上传文件
*
*/
public function uploadPut($url, $post = [], $timeout = 0,$content_type = '', $headers = [])
{
$ch = self::CURL($url, $timeout, $headers, $post, false, $has_curl_file=false, "PUT");
if (is_resource($ch) === true)
{
}
$response = curl_exec($ch);
if ($error = curl_error($ch))
{
$this->result['code'] = $this->fail_code;
$this->result['msg'] = $error;
}
else
{
$this->result['code'] = $this->success_code;
$this->result['response'] = $response;
}
curl_close($ch); // 关闭 cURL 释放资源
return $response;
}
static function CURL($url, $timeout, $headers = false, $post = false, $proxy = false, $has_curl_file = null, $method='GET', $curl_opts = array())
{
$ch = curl_init($url);
if (is_resource($ch) === true)
{
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TCP_NODELAY, true);
curl_setopt($ch, CURLOPT_NOSIGNAL, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_BUFFERSIZE, 10240);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $timeout);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
if ($headers !== false)
{
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
if ($proxy !== false)
{
curl_setopt($ch, CURLOPT_PROXY, $proxy);
}
if (!empty($curl_opts))
{
foreach ($curl_opts as $opt => $val)
{
curl_setopt($ch, $opt, $val);
}
}
switch ($method) {
case 'HEAD':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'HEAD');
break;
case 'GET':
break;
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
if($has_curl_file !== null)
{
curl_setopt($ch, CURLOPT_SAFE_UPLOAD, $has_curl_file);
}
break;
case 'DELETE':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
case 'PUT':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
break;
default:
die('httpUtil bad method');
break;
}
}
return $ch;
}
public function put($url, $params = [], $timeout = 0,$content_type = '', $headers = [], $retry = 0, $use_mon = true, $proxy = '')
{
try {
//验参
if (empty($url) || !is_array($params)) {
throw new \Exception('params error');
}
$this->log['url'] = $url;
$this->log['params'] = $params;
//构造client
$config = [];
if (!empty($retry) && is_int($retry)) {
$config['retry'] = $retry;
}
$client = (new Base())->initClient($config, $url, $params);
//构造配置
if (!empty($params)) {
if ($content_type == 'json') {
$options['json'] = $params;
} elseif ($content_type == 'multipart') {
$options['multipart'] = $params;
} else {
$options['form_params'] = $params;
}
}
if (!empty($timeout)) {
$options['timeout'] = $timeout / 1000;
}
if (!empty($headers)) {
$options['headers'] = $headers;
}
if (!empty($proxy)) {
$options['proxy'] = $proxy;
}
$options['on_stats'] = function (TransferStats $stats) use ($use_mon, $url) {
$this->result['http_code'] = $stats->getHandlerStat('http_code');
if (!empty($use_mon)) {
//MonUtil::proxyMon($url, $stats->getHandlerStat('http_code'), round($stats->getHandlerStat('total_time'),4) * 1000);
}
};
//put请求
//@method ResponseInterface put(string|UriInterface $uri, array $options = [])
$promise = $client->put($url, $options);
$promise->then(
function (Response $resp) {
//$ret = $this->log['response'] = $resp->getBody()->getContents();
$ret = empty(json_decode($ret, true)) ? $ret : json_decode($ret, true);
$this->result['code'] = $this->success_code;
$this->result['response'] = $ret;
},
function (RequestException $e) {
$this->log['response'] = $e->getMessage();
throw new \Exception($e->getMessage());
}
);
$promise->wait();
} catch (\Exception $e) {
$this->result['code'] = $this->fail_code;
$this->result['msg'] = $e->getMessage();
} finally {
$this->addLog('put');
return $this->result;
}
}
public function head($url, $params = [], $timeout = 0, $content_type = '',$headers = [], $retry = 0, $use_mon = true, $proxy = '')
{
try {
//验参
if (empty($url) || !is_array($params)) {
throw new \Exception('params error');
}
$this->log['url'] = $url;
$this->log['params'] = $params;
//构造client
$config = [];
if (!empty($retry) && is_int($retry)) {
$config['retry'] = $retry;
}
$this->client = (new Base())->initClient($config, $url, $params);
//构造配置
if (!empty($params)) {
if ($content_type == 'json') {
$options['json'] = $params;
} elseif ($content_type == 'multipart') {
$options['multipart'] = $params;
} else {
$options['form_params'] = $params;
}
}
if (!empty($timeout)) {
$options['timeout'] = $timeout / 1000;
}
if (!empty($headers)) {
$options['headers'] = $headers;
}
if (!empty($proxy)) {
$options['proxy'] = $proxy;
}
$options['on_stats'] = function (TransferStats $stats) use ($use_mon, $url) {
$this->result['http_code'] = $stats->getHandlerStat('http_code');
if (!empty($use_mon)) {
//MonUtil::proxyMon($url, $stats->getHandlerStat('http_code'), round($stats->getHandlerStat('total_time'),4) * 1000);
}
};
//异步head请求
// @method ResponseInterface head(string|UriInterface $uri, array $options = [])
$promise = $this->client->head($url, $options);
$promise->then(
function (Response $resp) {
$ret = $this->log['response'] = $resp->getBody()->getContents();
$ret = empty(json_decode($ret, true)) ? $ret : json_decode($ret, true);
$this->result['code'] = $this->success_code;
$this->result['response'] = $ret;
},
function (RequestException $e) {
$this->log['response'] = $e->getMessage();
throw new \Exception($e->getMessage());
}
);
$promise->wait();
} catch (\Exception $e) {
$this->result['code'] = $this->fail_code;
$this->result['msg'] = $e->getMessage();
} finally {
$this->addLog('head');
return $this->result;
}
}
/**
* 并发执行get请求
* @param array $urls [0=>xxx,1=>xxx]
* @param array $params [
* 0=> [
* 'key1' => val1,
* 'key2' => val2,
* ],
* 1=> [
* 'key1' => val1,
* 'key2' => val2,
* ]
* ]
* @param array $timeouts [0=>xxx,1=>xxx]
* @param int $retry 重试次数
* @param array $use_mons [0=>false,1=>false] 默认是true
* @param array $proxys [0=>'http://192.11.222.124:8000', 1=>'http://192.11.222.124:8000']
* @return array
*/
public function concurrencyGet($urls, $params = [], $timeouts = [], $retry = 0, $use_mons = [], $proxys = [])
{
try {
//验参
if (!is_array($urls) || empty($urls) || !is_array($params)) {
throw new \Exception('params error');
}
//构造client
$config = [
'concurrent' => true
];
if (!empty($retry) && is_int($retry)) {
$config['retry'] = $retry;
}
$this->client = (new Base())->initClient($config, $urls, $params);
//构造配置
$options = [];
foreach ($urls as $k => $url) {
$this->log['url'][$k] = $url;
$options[$k]['on_stats'] = function (TransferStats $stats) use ($k, $url, $use_mons) {
$this->con_result[$k]['http_code'] = $stats->getHandlerStat('http_code');
if (!isset($use_mons[$k]) || !empty($use_mons[$k])) {
//MonUtil::proxyMon($url, $stats->getHandlerStat('http_code'), round($stats->getHandlerStat('total_time'),4) * 1000);
}
};
}
if (!empty($params)) {
foreach ($params as $k => $v) {
$this->log['params'][$k] = $v;
$options[$k]['query'] = $v;
}
}
if (!empty($timeouts)) {
foreach ($timeouts as $k => $v) {
$options[$k]['timeout'] = $v / 1000;
}
}
if (!empty($proxys)) {
foreach ($proxys as $k => $v) {
$options[$k]['proxy'] = $v;
}
}
//异步并发get请求
$client = $this->client;
$requests = function ($urls, $options) use ($client) {
for ($i = 0; $i < count($urls); $i++) {
if (!isset($urls[$i]) || !isset($options[$i])) {
continue;
}
$each_url = $urls[$i];
$each_options = $options[$i];
yield function () use ($client, $each_url, $each_options) {
return $client->getAsync($each_url, $each_options);
};
}
};
$pool = new Pool($client, $requests($urls, $options), [
'concurrency' => $this->concurrency_num,
'fulfilled' => function (Response $response, $index) {
$ret = $response->getBody()->getContents();
$this->log['response'][$index] = $ret;
$ret = empty(json_decode($ret, true)) ? $ret : json_decode($ret, true);
$this->con_result[$index]['code'] = $this->success_code;
$this->con_result[$index]['response'] = $ret;
},
'rejected' => function (TransferException $te, $index) {
$this->con_result[$index]['code'] = $this->fail_code;
$this->con_result[$index]['msg'] = $te->getMessage();
$this->log['response'][$index] = $te->getMessage();
},
]);
$pool->promise()->wait();
} catch (\Exception $e) {
$this->con_result[]['code'] = $this->fail_code;
$this->con_result[]['msg'] = $e->getMessage();
$this->log['response'][] = $e->getMessage();
} finally {
$this->addLog('concurrencyGet');
return $this->con_result;
}
}
/**
* 并发执行post请求
* @param array $urls [ 0=>'url1', 1=>'url2']
* @param array $params [ 0=>[k1=>v1], 1=>[k2=>v2]]
* @param array $timeouts [ 0=>0.5, 1=>2]
* @param array $content_types [ 0=>'json']
* @param array $headers [
* 0 => [
* 'Content-type' => 'application/json',
* 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36'
* ]
* @param int $retry 重试次数
* @param array $use_mons [0=>false,1=>false] 默认是true
* @param array $proxys [0=>'http://192.11.222.124:8000', 1=>'http://192.11.222.124:8000']
* @return array
*/
public function concurrencyPost($urls, $params = [], $timeouts = [], $content_types = [], $headers = [], $retry = 0, $use_mons = [], $proxys = [])
{
try {
//验参
if (empty($urls) || !is_array($params)) {
throw new \Exception('params error');
}
//构造client
$config = [
'concurrent' => true
];
if (!empty($retry) && is_int($retry)) {
$config['retry'] = $retry;
}
$this->client = (new Base())->initClient($config, $urls, $params);
//构造配置
$options = [];
foreach ($urls as $k => $url) {
$this->log['url'][$k] = $url;
$options[$k]['on_stats'] = function (TransferStats $stats) use ($k, $url, $use_mons) {
$this->con_result[$k]['http_code'] = $stats->getHandlerStat('http_code');
//监控打点:模块名默认为第三方服务url 指标为请求返回的HTTP_code(默认-999)与总响应时间
if (!isset($use_mons[$k]) || !empty($use_mons[$k])) {
//MonUtil::proxyMon($url, $stats->getHandlerStat('http_code'), round($stats->getHandlerStat('total_time'),4) * 1000);
}
};
}
if (!empty($params)) {
foreach ($params as $k => $v) {
$this->log['params'][$k] = $v;
if (isset($content_types[$k]) && $content_types[$k] == 'json') {
$options[$k]['json'] = $v;
if (isset($headers[$k])) {
$options[$k]['headers'] = $headers[$k];
}
} elseif (isset($content_types[$k]) && $content_types[$k] == 'multipart') {
$options[$k]['multipart'] = $v;
} else {
$options[$k]['form_params'] = $v;
}
}
}
if (!empty($timeouts)) {
foreach ($timeouts as $k => $v) {
$options[$k]['timeout'] = $v / 1000;
}
}
if (!empty($proxys)) {
foreach ($proxys as $k => $v) {
$options[$k]['proxy'] = $v;
}
}
//异步并发post请求
$client = $this->client;
$requests = function ($urls, $options) use ($client) {
for ($i = 0; $i < count($urls); $i++) {
if (!isset($urls[$i]) || !isset($options[$i])) {
continue;
}
$each_url = $urls[$i];
$each_options = $options[$i];
yield function () use ($client, $each_url, $each_options) {
return $client->postAsync($each_url, $each_options);
};
}
};
$pool = new Pool($client, $requests($urls, $options), [
'concurrency' => $this->concurrency_num,
'fulfilled' => function (Response $response, $index) {
$ret = $response->getBody()->getContents();
$this->log['response'][$index] = $ret;
$ret = empty(json_decode($ret, true)) ? $ret : json_decode($ret, true);
$this->con_result[$index]['code'] = $this->success_code;
$this->con_result[$index]['response'] = $ret;
},
'rejected' => function (TransferException $te, $index) {
$this->con_result[$index]['code'] = $this->fail_code;
$this->con_result[$index]['msg'] = $te->getMessage();
$this->log['response'][$index] = $te->getMessage();
},
]);
$pool->promise()->wait();
} catch (\Exception $e) {
$this->con_result[]['code'] = $this->fail_code;
$this->con_result[]['msg'] = $e->getMessage();
$this->log['response'][] = $e->getMessage();
} finally {
$this->addLog('concurrencyPost');
return $this->con_result;
}
}
/**
* 并发执行post+get混合请求
* @param array $urls [ 0=>'url1', 1=>'url2']
* @param array $methods [ 0=>'GET', 1=>'POST']
* @param array $params [ 0=>[k1=>v1], 1=>[k2=>v2]]
* @param array $timeouts [ 0=>0.5, 1=>2]
* @param array $content_types [ 0=>'json']
* @param array $headers [
* 0 => [
* 'Content-type' => 'application/json',
* 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36'
* ]
* @param int $retry 重试次数
* @param array $use_mons [0=>false,1=>false] 默认是true
* @param array $proxys [0=>'http://192.11.222.124:8000', 1=>'http://192.11.222.124:8000']
* @return array
*/
public function concurrencyBlend($urls, $methods, $params = [], $timeouts = [], $content_types = [], $headers = [], $retry = 0, $use_mons = [], $proxys = [])
{
try {
//验参
if (empty($urls) || empty($methods) || !is_array($params)) {
throw new \Exception('params error');
}
//构造client
$config = [
'concurrent' => true
];
if (!empty($retry) && is_int($retry)) {
$config['retry'] = $retry;
}
$this->client = (new Base())->initClient($config, $urls, $params);
//构造配置
$options = [];
foreach ($urls as $k => $url) {
$this->log['url'][$k] = $url;
$options[$k]['on_stats'] = function (TransferStats $stats) use ($k, $url, $use_mons) {
$this->con_result[$k]['http_code'] = $stats->getHandlerStat('http_code');
if (!isset($use_mons[$k]) || !empty($use_mons[$k])) {
//MonUtil::proxyMon($url, $stats->getHandlerStat('http_code'), round($stats->getHandlerStat('total_time'),4) * 1000);
}
};
}
if (!empty($params)) {
foreach ($params as $k => $v) {
if (!isset($methods[$k])) {
continue;
}
$this->log['params'][$k] = $v;
if ($methods[$k] == 'POST') {
if (isset($content_types[$k]) && $content_types[$k] == 'json') {
$options[$k]['json'] = $v;
if (isset($headers[$k])) {
$options[$k]['headers'] = $headers[$k];
}
} elseif (isset($content_types[$k]) && $content_types[$k] == 'multipart') {
$options[$k]['multipart'] = $v;
} else {
$options[$k]['form_params'] = $v;
}
} else {
$options[$k]['query'] = $v;
}
}
}
if (!empty($timeouts)) {
foreach ($timeouts as $k => $v) {
$options[$k]['timeout'] = $v / 1000;
}
}
if (!empty($proxys)) {
foreach ($proxys as $k => $v) {
$options[$k]['proxy'] = $v;
}
}
//异步并发请求
$client = $this->client;
$requests = function ($urls, $methods, $options) use ($client) {
for ($i = 0; $i < count($urls); $i++) {
if (!isset($urls[$i]) || !isset($methods[$i]) || !isset($options[$i]) || !in_array($methods[$i], ['GET', 'POST'])) {
continue;
}
$each_url = $urls[$i];
$each_method = $methods[$i];
$each_options = $options[$i];
yield function () use ($client, $each_url, $each_method, $each_options) {
if ($each_method == 'POST') {
return $client->postAsync($each_url, $each_options);
}
return $client->getAsync($each_url, $each_options);
};
}
};
$pool = new Pool($client, $requests($urls, $methods, $options), [
'concurrency' => $this->concurrency_num,
'fulfilled' => function (Response $response, $index) {
$ret = $response->getBody()->getContents();
$this->log['response'][$index] = $ret;
$ret = empty(json_decode($ret, true)) ? $ret : json_decode($ret, true);
$this->con_result[$index]['code'] = $this->success_code;
$this->con_result[$index]['response'] = $ret;
},
'rejected' => function (TransferException $te, $index) {
$this->con_result[$index]['code'] = $this->fail_code;
$this->con_result[$index]['msg'] = $te->getMessage();
$this->log['response'][$index] = $te->getMessage();
},
]);
$pool->promise()->wait();
} catch (\Exception $e) {
$this->con_result[]['code'] = $this->fail_code;
$this->con_result[]['msg'] = $e->getMessage();
$this->log['response'][] = $e->getMessage();
} finally {
$this->addLog('concurrencyBlend');
return $this->con_result;
}
}
private function addLog($method)
{
//过滤无意义的日志
$log = true;
$excludes = config('request', 'log.exclude');
if (!empty($excludes)) {
foreach ($excludes as $exclude) {
if (is_array($this->log['url'])) {
foreach ($this->log['url'] as $url) {
if (strpos($url, $exclude) !== false) {
$log = false;
break;
}
}
} else {
if (strpos($this->log['url'], $exclude) !== false) {
$log = false;
break;
}
}
}
}
if (!empty($log)) {
$this->log['params'] = $this->change($this->log['params']);
FileLog::info($method, json_encode($this->log));
}
}
/**
* 过滤resource类型字段
* @param array $arr
* @return array
*/
private function change($arr)
{
foreach ($arr as $k => $v) {
if (is_array($v)) {
$arr[$k] = $this->change($v);
} else {
if (strpos(gettype($v), 'resource') !== false) {
$arr[$k] = gettype($v);
}
}
}
return $arr;
}
}
<?php
namespace Api\PhpUtils\Hystrix;
use Odesk\Phystrix\StateStorageInterface;
class ApcuStateStorage implements StateStorageInterface
{
const BUCKET_EXPIRE_SECONDS = 120;
const CACHE_PREFIX = 'phystrix_cb_';
const OPENED_NAME = 'opened';
const SINGLE_TEST_BLOCKED = 'single_test_blocked';
/**
* Constructor
*/
public function __construct()
{
if (!extension_loaded('apcu')) {
throw new Exception\ApcNotLoadedException('"apcu" PHP extension is required for Phystrix to work');
}
}
/**
* Prepends cache prefix and filters out invalid characters
*
* @param string $name
* @return string
*/
protected function prefix($name)
{
return self::CACHE_PREFIX . $name;
}
/**
* Returns counter value for the given bucket
*
* @param string $commandKey
* @param string $type
* @param integer $index
* @return integer
*/
public function getBucket($commandKey, $type, $index)
{
$bucketName = $this->prefix($commandKey . '_' . $type . '_' . $index);
return apcu_fetch($bucketName);
}
/**
* Increments counter value for the given bucket
*
* @param string $commandKey
* @param string $type
* @param integer $index
*/
public function incrementBucket($commandKey, $type, $index)
{
$bucketName = $this->prefix($commandKey . '_' . $type . '_' . $index);
if (!apcu_add($bucketName, 1, self::BUCKET_EXPIRE_SECONDS)) {
apcu_inc($bucketName);
}
}
/**
* If the given bucket is found, sets counter value to 0.
*
* @param string $commandKey Circuit breaker key
* @param integer $type
* @param integer $index
*/
public function resetBucket($commandKey, $type, $index)
{
$bucketName = $this->prefix($commandKey . '_' . $type . '_' . $index);
if (apcu_exists($bucketName)) {
apcu_store($bucketName, 0, self::BUCKET_EXPIRE_SECONDS);
}
}
/**
* Marks the given circuit as open
*
* @param string $commandKey Circuit key
* @param integer $sleepingWindowInMilliseconds In how much time we should allow a single test
*/
public function openCircuit($commandKey, $sleepingWindowInMilliseconds)
{
$openedKey = $this->prefix($commandKey . self::OPENED_NAME);
$singleTestFlagKey = $this->prefix($commandKey . self::SINGLE_TEST_BLOCKED);
apcu_store($openedKey, true);
// the single test blocked flag will expire automatically in $sleepingWindowInMilliseconds
// thus allowing us a single test. Notice, APC doesn't allow us to use
// expire time less than a second.
$sleepingWindowInSeconds = ceil($sleepingWindowInMilliseconds / 1000);
apcu_add($singleTestFlagKey, true, $sleepingWindowInSeconds);
}
/**
* Whether a single test is allowed
*
* @param string $commandKey Circuit breaker key
* @param integer $sleepingWindowInMilliseconds In how much time we should allow the next single test
* @return boolean
*/
public function allowSingleTest($commandKey, $sleepingWindowInMilliseconds)
{
$singleTestFlagKey = $this->prefix($commandKey . self::SINGLE_TEST_BLOCKED);
// using 'add' enforces thread safety.
$sleepingWindowInSeconds = ceil($sleepingWindowInMilliseconds / 1000);
// another APC limitation is that within the current request variables will never expire.
return (boolean) apcu_add($singleTestFlagKey, true, $sleepingWindowInSeconds);
}
/**
* Whether a circuit is open
*
* @param string $commandKey Circuit breaker key
* @return boolean
*/
public function isCircuitOpen($commandKey)
{
$openedKey = $this->prefix($commandKey . self::OPENED_NAME);
return (boolean) apcu_fetch($openedKey);
}
/**
* Marks the given circuit as closed
*
* @param string $commandKey Circuit key
*/
public function closeCircuit($commandKey)
{
$openedKey = $this->prefix($commandKey . self::OPENED_NAME);
apcu_store($openedKey, false);
}
}
<?php
namespace Api\PhpUtils\Hystrix;
use Odesk\Phystrix\AbstractCommand;
class DemoCommand extends AbstractCommand
{
private $b;
private $a;
public function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
}
protected function run()
{
$arr = [0,1];
shuffle($arr);
$int = array_pop($arr);
if($int){
throw new \Exception();
}
return $this->a.$this->b;
}
protected function getFallback()
{
return "Fallback";
}
}
\ No newline at end of file
<?php
namespace Api\PhpUtils\Hystrix;
use Odesk\Phystrix\AbstractCommand;
use Zend\Config\Config;
use Odesk\Phystrix\ApcStateStorage;
use Odesk\Phystrix\CircuitBreakerFactory;
use Odesk\Phystrix\CommandMetricsFactory;
use Odesk\Phystrix\CommandFactory;
class Hystrix
{
private $phystrix;
public function __construct(){
$config = new Config($this->getConfig());
$stateStorage = new ApcuStateStorage();
$circuitBreakerFactory = new CircuitBreakerFactory($stateStorage);
$commandMetricsFactory = new CommandMetricsFactory($stateStorage);
$this->phystrix = new CommandFactory(
$config, new \Zend\Di\ServiceLocator(), $circuitBreakerFactory, $commandMetricsFactory,
new \Odesk\Phystrix\RequestCache(), new \Odesk\Phystrix\RequestLog()
);
}
public function run($class, ...$params){
$command = $this->phystrix->getCommand($class, ...$params);
return $command->execute();
}
private function getConfig(){
return [
'default' => [
'fallback' => array(
'enabled' => config("request","hystrix.fallback.enabled"),
),
'circuitBreaker' => array(
'enabled' => config("request","hystrix.circuitBreaker.enabled"),
// 失败率达到多少百分比后熔断
'errorThresholdPercentage' => config("request","hystrix.circuitBreaker.errorThresholdPercentage"),
// 是否强制开启熔断
'forceOpen' => config("request","hystrix.circuitBreaker.forceOpen"),
// 是否强制关闭熔断
'forceClosed' => config("request","hystrix.circuitBreaker.forceClosed"),
// 一个统计窗口内熔断触发的最小个数/10s
'requestVolumeThreshold' => config("request","hystrix.circuitBreaker.requestVolumeThreshold"),
// 熔断多少秒后去尝试请求
'sleepWindowInMilliseconds' => config("request","hystrix.circuitBreaker.sleepWindowInMilliseconds"),
),
'metrics' => array(
// 采样时间间隔
'healthSnapshotIntervalInMilliseconds' => config("request","hystrix.metrics.healthSnapshotIntervalInMilliseconds"),
// 设置统计滚动窗口的长度,以毫秒为单位。用于监控和熔断器。
'rollingStatisticalWindowInMilliseconds' => config("request","hystrix.metrics.rollingStatisticalWindowInMilliseconds"),
// 设置统计窗口的桶数量
'rollingStatisticalWindowBuckets' => config("request","hystrix.metrics.rollingStatisticalWindowBuckets"),
),
'requestCache' => array(
// 设置是否缓存请求
'enabled' => config("request","hystrix.requestCache.enabled"),
),
'requestLog' => array(
// 设置HystrixCommand执行和事件是否打印到HystrixRequestLog中
'enabled' => config("request","hystrix.requestLog.enabled"),
)
]
];
}
}
\ No newline at end of file
<?php
namespace Api\PhpUtils\Kafka;
use Api\PhpUtils\Log\FileLog;
/**
* Kafka工具类
*
* rdkafka.so verion 3.0.5
*/
class KafkaUtil
{
private static $instances = [];
private $serverName = ''; // yaconf中kafka.ini配置的业务名,比如viewlog
/**
* @var \RdKafka\Producer
*/
private $producer = null;
/**
* @var \RdKafka\Conf
*/
private $conf = null;
private $confSet = [];
private $confFunction = [];
/**
* @var \RdKafka\TopicConf
*/
private $topicConf;
private $topicConfSet = [];
private $topicConfFunction = [];
private $brokers = '';
private $topicName = '';
private $context = [];
private $config = [];
/**
* 获取实例
*
* @param string $serverName kafka.ini中的服务名
* @param array $options 其他参数数组,优先级高于yaconf中的相同配置
* @param bool $options['context'] 上下文信息
* @return mixed
*/
public static function getInstance($serverName = '', array $options = [])
{
$preKey = 'kakfa:';
if (empty($options)) {
$instancesKey = $preKey . $serverName;
} else {
$instancesKey = $preKey . $serverName . ':' . json_encode($options);
}
if (isset(self::$instances[$instancesKey])) {
return self::$instances[$instancesKey];
}
return self::$instances[$instancesKey] = new Self($serverName, $options);
}
private function __construct($serverName = '', array $options = [])
{
$this->serverName = $serverName;
$this->config = config('kafka', $this->serverName);
if (!empty($this->config)) {
isset($this->config['topic']) && $this->setTopicName($this->config['topic']);
isset($this->config['broker_list']) && $this->setBrokers($this->config['broker_list']);
}
$this->context = $options['context'] ?? [];
try{
$this->conf = new \RdKafka\Conf();
$this->topicConf = new \RdKafka\TopicConf();
$this->defaultConfProducer();
$this->defaultTopicConfProducer();
} catch(\Exception $exception){
FileLog::error('kafkaUtil __construct-Exception:' . $exception->getMessage());
}
}
/**
* Produce and send a single message
*
* @param string $payload
* @param string $key
* @param integer $partition
* @return void
*/
public function produce($payload, $key = NULL, $partition = RD_KAFKA_PARTITION_UA)
{
try{
is_null($this->producer) && $this->initProducer();
$topicConf = $this->runTopicConf()->getTopicConf();
$topicName = $this->getTopicName();
$topic = $this->producer->newTopic($topicName, $topicConf);
$topic->produce($partition, 0, $payload, $key);
while(($len = $this->producer->getOutQLen()) > 0){
// 1.Kafka轮询一次就相当于拉取(poll)一定时间段broker中可消费的数据, 在这个指定时间段里拉取,时间到了就立刻返回数据。
// 2.例如poll(5000): 即在5s中内拉去的数据返回到消费者端。
$this->producer->poll(5);
}
return true;
} catch(\Exception $exception){
FileLog::error('kafkaUtil producer run-Exception:' . $exception->getMessage());
return false;
}
}
/**
* 初始化producer
*
* @return $this
*/
public function initProducer()
{
$conf = $this->runConf()->getConf();
$this->producer = new \RdKafka\Producer($conf);
$brokers = $this->getBrokers();
$this->producer->addBrokers($brokers);
return $this;
}
public function getConf()
{
return $this->conf;
}
public function getTopicConf()
{
return $this->topicConf;
}
public function getBrokers()
{
return $this->brokers;
}
public function setBrokers($brokers)
{
if(!is_array($brokers) && !is_object($brokers)){
$brokers = explode(',', $brokers);
}
$brokers = implode(',', $brokers);
$this->brokers = $brokers;
return $this;
}
public function getTopicName()
{
return $this->topicName;
}
public function setTopicName($topicName)
{
$this->topicName = $topicName;
return $this;
}
public function setConfStatsCb($callback)
{
$this->addConfFunction('setStatsCb', $callback);
return $this;
}
public function setConfDrMsgCb($callback)
{
$this->addConfFunction('setDrMsgCb', $callback);
return $this;
}
public function setConfErrorCb($callback)
{
$this->addConfFunction('setErrorCb', $callback);
return $this;
}
public function setConf($name, $value)
{
$this->addConfSet($name, $value);
return $this;
}
public function addConfSet($name, $value, $default = FALSE)
{
if($default && isset($this->confSet[$name])){
return $this;
}
$this->confSet[$name] = $value;
return $this;
}
public function addConfFunction($function, $callback, $default = FALSE)
{
if($default && isset($this->confFunction[$function])){
return $this;
}
$this->confFunction[$function] = $callback;
return $this;
}
public function setTopicConf($name, $value)
{
$this->addTopicConfSet($name, $value);
return $this;
}
public function addTopicConfSet($name, $value, $default = FALSE)
{
if($default && isset($this->topicConfSet[$name])){
return $this;
}
$this->topicConfSet[$name] = $value;
return $this;
}
public function addTopicConfFunction($function, $callback, $default = FALSE)
{
if($default && isset($this->topicConfFunction[$function])){
return $this;
}
$this->topicConfFunction[$function] = $callback;
return $this;
}
public function runConf()
{
if($this->confSet){
foreach($this->confSet as $key => $value){
$this->conf->set($key, $value);
}
}
if($this->confFunction){
foreach($this->confFunction as $key => $value){
if(method_exists($this->conf, $key)){
call_user_func([$this->conf, $key], $value);
}
}
}
return $this;
}
public function runTopicConf()
{
if($this->topicConfSet){
foreach($this->topicConfSet as $key => $value){
$this->topicConf->set($key, $value);
}
}
if($this->topicConfFunction){
foreach($this->topicConfFunction as $key => $value){
if(method_exists($this->topicConf, $key)){
call_user_func([$this->topicConf, $key], $value);
}
}
}
return $this;
}
/**
* 初始化通用配置,暂不使用,setStatsCb(rdkafka > 3.0.5)
*
* @return void
*/
public function defaultInit()
{
$this->addConfFunction('setStatsCb', function ($kafka, $json, $json_len) {
// FileLog::error('setStatsCb $json=' . $json);
}, TRUE);
$this->addConfFunction('setErrorCb', function ($kafka, $err, $reason) {
// FileLog::error(sprintf("setErrorCb (error_ori:%s)(error: %s) (reason: %s)", $err, rd_kafka_err2str($err), $reason));
}, TRUE);
return $this;
}
/**
* 初始化生产者producer配置
*
* @return $this
*/
public function defaultConfProducer()
{
$context = $this->context;
$this->addConfFunction('setDrMsgCb', function ($kafka, \RdKafka\Message $message) use ($context) {
// var_dump('111----');
if($message->err){
$msg = $message->errstr() . ' topic : ' . $message->topic_name . (empty($context) ? (' payload : ' . $message->payload) : (' context: ' . json_encode($context)));
FileLog::error('kafka produce callback error', $msg);
} else{
$msg = ' topic : ' . $message->topic_name . (empty($context) ? (' payload : ' . $message->payload) : (' context: ' . json_encode($context)));
FileLog::info('kafka produce callback success', $msg);
}
}, TRUE);
$this->addConfFunction('setErrorCb', function ($kafka, $err, $reason) {
// FileLog::error('kafka error callback', rd_kafka_err2str($err).' reason: '.$reason);
if ($err == RD_KAFKA_RESP_ERR__ALL_BROKERS_DOWN) {
return false;
}
}, TRUE);
$this->addConfSet('socket.timeout.ms', 50, TRUE);
$this->addConfSet('socket.blocking.max.ms', 50, TRUE);
$this->addConfSet('topic.metadata.refresh.sparse', TRUE, TRUE);
$this->addConfSet('topic.metadata.refresh.interval.ms', 600, TRUE);
$this->addConfSet('socket.nagle.disable', TRUE, TRUE);
$this->addConfSet('message.timeout.ms', 50, TRUE);
$this->addConfSet('log_level', LOG_WARNING, TRUE);
if(function_exists('pcntl_sigprocmask')){
pcntl_sigprocmask(SIG_BLOCK, array(SIGIO));
$this->addConfSet('internal.termination.signal', SIGIO, TRUE);//设置kafka客户端线程在其完成后立即终止
} else{
$this->addConfSet('queue.buffering.max.ms', 1, TRUE);//确保消息尽快发送
}
return $this;
}
/**
* 初始化生产者topic配置
*
* @return $this
*/
public function defaultTopicConfProducer()
{
$this->addTopicConfSet('request.required.acks', 1, TRUE);
return $this;
}
}
#Kafka工具库
###旧版本使用示例(KafkaProducer has been DEPRECATED)
```php
$service = KafkaProducer::getInstance();
$message= json_encode(array('test' => 'kafka produce', 'reason' => 'send message'), JSON_UNESCAPED_UNICODE);
var_dump($service);
$service->produce('10.120.25.19:9092,10.120.24.27:9092,10.120.25.6:9092', 'push_content_sync', $message);
```
###新版本使用示例
```php
public function kafkaAction()
{
$topicName = 'topicA';
$setConfDrMsgCb = function ($kafka, \RdKafka\Message $message) {
echo '打印消息:' . "\n";
var_export($message);
echo "\n";
if($message->err){
//@todo 生产失败的逻辑处理
} else{
//@todo 生产成功的逻辑处理
}
};
$setErrorCbCallback = function ($kafka, $err, $reason) {
echo sprintf("setErrorCb (error_ori:%s)(error: %s) (reason: %s)", $err, rd_kafka_err2str($err), $reason);
};
// $kafka = KafkaUtil::getInstance('test', ['context' => ['contextArr']]);
// $ret = $kafka->produce('eee');
$brokers = '127.0.0.1:9092';
$kafka = KafkaUtil::getInstance();
$ret = $kafka->setBrokers($brokers)->setTopicName('topicA')->produce('ccc');
var_dump($kafka);
exit;
//设置brokers-支持数组或者字符串
$kafka->setBrokers($brokers)
//设置topic
->setTopicName($topicName)
//setConfDrMsgCb--设置生产消息失败与成功的回调,默认初始化时会调用该事件,如果自定义后就会覆盖默认的
->setConfDrMsgCb($setConfDrMsgCb)
//setConfErrorCb--设置错误回调,如若不设置默认会走默认逻辑
->setConfErrorCb($setErrorCbCallback)
//支持连贯用法生产多条数据并且是不同的topic
->run('message111', $topicName);
}
```
<?php
namespace Api\PhpUtils\Ksy;
use Api\PhpUtils\Http\Request;
/**
* 金山云ks3相关
* 仅实现了签名及删除单个方法,其他未实现
* ks3直接访问地址: https://ks3-cn-beijing.ksyun.com/article-yidian/article/0JRvMeHk
*/
class Ks3Util
{
const KS3_BASE_URL = 'ks3-cn-beijing.ksyun.com';
const FALLBACK_BUIDLER_BUCKET = 'article-yidian';
const KS_DATE_SCHEME = 'D, d M Y H:i:s \G\M\T';
const KSYUN_SK = "OMw7UAOkrLVsRevuP44ctAlfzriDzUm9+JvOFoYuiSoLgaLJWi+kjUa+4G3EWXFakQ==";
const KSYUN_AK = "AKLTnATdoXUnR72IDSGgN2WSOQ";
const YIDIAN_PROXY = true;
//删除fallback builder 对于ks3节点的文章
public static function delete_doc($docid, $timeout = 1000)
{
return self::delete(self::FALLBACK_BUIDLER_BUCKET, 'article/'.$docid, $timeout);
}
public static function put_object($data,$bucket,$object_key, $timeout = 1000)
{
$url = self::get_url($bucket).'/'.$object_key;
$canonicalized_resource = self::get_canonicalized_resource($bucket, $object_key);
$headers = array(
'Host' => self::get_url($bucket),
'Date' => gmdate(self::KS_DATE_SCHEME),
'Content-Type' => 'multipart/form-data',
'Referer' => $url,
'x-kss-acl' => 'public-read',
//'Expect' => '100-continue',
);
$headers['Authorization'] = self::sign('PUT', $canonicalized_resource, $headers);
foreach ($headers as $key => $value)
{
$header[] = $key.':'.$value;
}
$request = new Request();
$str = $request->uploadPut($url, $data, $timeout, 1, $header, self::YIDIAN_PROXY);
return $str;
}
//查询文章在ks3是否存在,返回金山云返回的head http状态吗
public static function exist($bucket, $object_key, $timeout = 1000)
{
$url = self::get_url($bucket).'/'.$object_key;
$canonicalized_resource = self::get_canonicalized_resource($bucket, $object_key);
$headers = array(
'Host' => self::get_url($bucket),
'Date' => gmdate(self::KS_DATE_SCHEME),
'Content-Type' => 'application/xml',
);
$headers['Authorization'] = self::sign('HEAD', $canonicalized_resource, $headers);
foreach ($headers as $key => $value)
{
$header[] = $key.':'.$value;
}
$request = new Request();
$str = $request->head($url, $timeout, 1, $header, self::YIDIAN_PROXY);
return $str;
}
//删除ks3 节点指定bucket中的object
private static function delete($bucket, $object_key, $timeout)
{
$url = self::get_url($bucket).'/'.$object_key;
$headers = array(
'Host' => self::get_url($bucket),
'Accept' => '*/*',
'Accept-Encoding' => 'deflate, gzip',
'Referer' => $url,
'Content-Type' => 'application/xml',
'Date' => gmdate(self::KS_DATE_SCHEME),
'Content-Length' => 0,
);
$canonicalized_resource = self::get_canonicalized_resource($bucket, $object_key);
$headers['Authorization'] = self::sign('DELETE', $canonicalized_resource, $headers);
foreach ($headers as $key => $value)
{
$header[] = $key.':'.$value;
}
$request = new Request();
$str = $request->delete($url, $timeout, 1, $header, self::YIDIAN_PROXY);
return $str;
}
private static function get_url($bucket)
{
return $bucket.'.'.self::KS3_BASE_URL;
}
private static function sign($method,$canonicalized_resource, $headers=array(), $query = array())
{
$canonicalized_kss_headers = self::format_headers($headers);
$tosign = $method . "\n"
. '' . "\n"
. $headers['Content-Type'] . "\n"
. $headers['Date'] . "\n"
. $canonicalized_kss_headers
. $canonicalized_resource;
$signature = base64_encode(hash_hmac('sha1', $tosign, self::KSYUN_SK, true));
$signature = 'KSS ' . self::KSYUN_AK . ':' . $signature;
return $signature;
}
//生成签名所需CanonicalizedKssHeaders,详见https://ks3.ksyun.com/doc/api/index.html
private static function format_headers(array $headers)
{
if( empty($headers) )
{
return '';
}
foreach ($headers as $key => $header) {
if( substr($key,0,6) == 'x-kss-' )
{
$headers_to_sign[strtolower($key)] = $header;
}
}
if( empty($headers_to_sign) )
{
return '';
}
ksort($headers_to_sign);
$ret = '';
foreach ($headers_to_sign as $key => $value)
{
$ret = $key.':'.$value."\n";
}
return $ret;
}
//
private static function get_canonicalized_resource($bucket, $object_key)
{
return '/'.$bucket.'/'.$object_key;
}
}
Lock工具库
\ No newline at end of file
<?php
namespace Api\PhpUtils\Log;
class DaemoLog
{
public static function info($task_name, $msg)
{
$msg = $task_name.' '.posix_getpid().' [INFO]: '.$msg;
error_log($msg);
}
public static function error($task_name, $msg)
{
$msg = $task_name.' '.posix_getpid().' [ERROR]: '.$msg;
error_log($msg);
}
}
\ No newline at end of file
<?php
namespace Api\PhpUtils\Log;
use Api\PhpUtils\Message\Email;
class FileLog
{
/**
* 用于记录info级别的错误
* 会记录请求上下文,不会发生报警邮件
* @param string $signature 本条日志签名
* @param string $detail_info 日志详细信息
* @param bool $with_access_log
*/
public static function info($signature, $detail_info = '', $with_access_log = false)
{
$log = 'PHP User_info: [' . $signature . ']' . ' [detail info:] ' . $detail_info;
if ($with_access_log) {
$log .= ' [request info:] ' . self::accessLog();
}
self::writeInfoLog($log);
}
/**
* 用于记录waring级别的错误,在日志分析时此级别日志会使用signature进行聚合
* 会记录请求上下文,不会发生报警邮件
* @param string $signature 本条日志签名
* @param string $detail_info 日志详细信息
* @param \Exception $exception 异常信息
*/
public static function waring($signature, $detail_info = '', $exception = null)
{
$log = 'PHP User_warning: [' . $signature . ']' . ' [detail info:] ' . $detail_info . ' [request info:] ' . self::accessLog();
if ($exception != '') {
$exception_info = 'In ' . $exception->getFile() . ' on line ' . $exception->getLine() . ' ' . $exception->getCode() . ': ' . $exception->getMessage();
$log .= ' [exception info: ]' . $exception_info;
}
self::writeWarningLog($log);
}
/**
* 用于记录error级别的错误,在日志分析时此级别日志会使用signatrue进行聚合
* 会记录请求上下文,如果设置了mail_to参数会发生报警邮件
* @param string $signature 本条日志签名
* @param string $detail_info 日志详细信息
* @param \Exception $exception 异常信息
* @param array|string $mail_to 邮件接受者
*/
public static function error($signature, $detail_info = '', $exception = null, $mail_to = '')
{
$log = 'PHP User_error: [' . $signature . ']' . ' [detail info:] ' . $detail_info . ' [request info:] ' . self::accessLog();
if ($exception != '') {
$exception_info = ' [exception info: ] In ' . $exception->getFile() . ' on line ' . $exception->getLine() . ' ' . $exception->getCode() . ': ' . $exception->getMessage();
} else {
$exception_info = '';
}
$log .= ' [exception info: ]' . $exception_info;
error_log($log);
if (!empty($mail_to)) {
$subject = 'App api #' . $signature . '# ' . $_SERVER['SERVER_NAME'] . ' (' . $_SERVER['SERVER_ADDR'] . ') Alert Message';
$body = 'Error: ' . $signature . "\n\n";
$body .= 'Detail info: ' . $detail_info . "\n\n";
$body .= 'Exception info: ' . $exception_info . "\n\n";
$body .= 'Request info: ' . self::accessLog() . "\n\n";
$body .= 'Machine: ' . gethostname();
if (!is_array($mail_to)) {
$mail_to = [$mail_to];
}
foreach ($mail_to as $mail) {
$key = md5(sprintf("%s,%s", $mail, md5($signature)));
if (self::shouldSendEmail($key) === true) {
Email::sendMail('noreply@yidian-inc.com', $mail, $subject, $body);
}
}
}
}
private static function accessLog()
{
$url = 'http://' . $_SERVER['SERVER_NAME'] . $_SERVER['SCRIPT_NAME'];
$query = http_build_query($_GET, '', '&');
$post_data = json_encode($_POST);
$agent = $_SERVER['HTTP_USER_AGENT'] ?? '-';
$cookie = $_COOKIE['JSESSIONID'] ?? '-';
return sprintf('%s?%s "%s" "%s" "%s" "%s"', $url, $query, $post_data, $agent, self::ip(), $cookie);
}
private static function writeInfoLog($log)
{
$log = '[' . date('d-M-Y H:i:s', time()) . ' ' . date_default_timezone_get() . '] ' . $log;
$log_file = fopen(dirname(ini_get('error_log')) . '/php-infos.log', 'a+');
fwrite($log_file, $log . "\n");
fclose($log_file);
}
private static function writeWarningLog($log)
{
$log = '[' . date('d-M-Y H:i:s', time()) . ' ' . date_default_timezone_get() . '] ' . $log;
$log_file = fopen(dirname(ini_get('error_log')) . '/php-warnings.log', 'a+');
fwrite($log_file, $log . "\n");
fclose($log_file);
}
private static function ip()
{
if (isset($_SERVER['IPV6_REMOTE_ADDR']) && !empty($_SERVER['IPV6_REMOTE_ADDR']) && trim($_SERVER['IPV6_REMOTE_ADDR']) !== '-') { // 如果没有ipv6地址, 运维会传一个占位符“-”
$ips = $_SERVER['IPV6_REMOTE_ADDR'];
} elseif (!empty($_SERVER['HTTP_CDN_SRC_IP'])) {
$ips = $_SERVER['HTTP_CDN_SRC_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = $_SERVER['HTTP_X_FORWARDED_FOR'];
} elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ips = $_SERVER['HTTP_CLIENT_IP'];
} else {
$ips = $_SERVER['REMOTE_ADDR'];
}
$ip = explode(',', $ips);
return $ip[0];
}
private static function shouldSendEmail($key)
{
$result = true;
// $cache = new CommonCacheUtil(RedisUtil::CODIS_CLUSTER_ACTION);
// if ($cache->add($key, true, 60, CacheUtil::PREFIX_SEND_MAIL) === false) {
// $result = false;
// }
return $result;
}
}
Log工具库
\ No newline at end of file
<?php
namespace Api\PhpUtils\Message;
class Email
{
/**
* @param string $from 发件人
* @param string|array $to 收件人, 支持多个收件人
* @param string $subject 邮件主题
* @param string $body 邮件内容
* @param string $smtp_server
* @param int $smtp_port
* @param string $content_type body内容的格式和编码, 可支持html格式邮件 $content_type="Content-Type: text/html; charset=\"utf-8\"\r\n\r\n"
* @return bool
*/
public static function sendMail($from, $to, $subject, $body, $smtp_server = 'smtp.yidian.com', $smtp_port = 25, $content_type = "Content-Type: text/plain; charset=\"utf-8\"\r\n\r\n")
{
if (empty($to)) {
return false;
}
if (empty($from)) {
$from = 'noreply@yidian-inc.com';
}
try {
$email = explode('@', $from);
$domain = $email[1];
$sock = fsockopen($smtp_server, $smtp_port, $errno, $errstr, 1);
fwrite($sock, "HELO " . $domain . "\r\n");
fgets($sock);
fwrite($sock, "MAIL FROM:<" . $from . ">\r\n");
fgets($sock);
if (!is_array($to)) {
$to = [$to];
}
foreach ($to as $t) {
fwrite($sock, "RCPT TO:<" . $t . ">\r\n");
}
fgets($sock);
fwrite($sock, "DATA\r\n");
fgets($sock);
fwrite($sock, "Subject: " . $subject . "\r\n");
fwrite($sock, "From: <" . $from . ">\r\n");
foreach ($to as $t) {
fwrite($sock, "To: <" . $t . ">\r\n");
}
fwrite($sock, $content_type);
fwrite($sock, $body . "\r\n.\r\n");
fgets($sock);
fwrite($sock, "QUIT\r\n");
fgets($sock);
fclose($sock);
} catch (\Exception $e) {
return false;
}
}
}
Email工具库
\ No newline at end of file
<?php
/**
* Description of MonUtil.php
*
* @author caoyunmeng
* Date: 2021/2/2
* Time: 2:27 PM
*/
namespace Api\PhpUtils\Mon;
class MonUtil{
/**
* 向安装有ydbot服务的机器发送信息,进行监控打点
*
* @param string $send_msg 打点信息
* @param string $ip 服务器ip,默认为宿主机ip(172.17.0.1)
* @param string $port 端口,默认为15688
* @return string
*/
public static function base($send_msg, $ip = '172.17.0.1', $port = '15688'){
if(isset($_SERVER['YIDIAN_LOCAL_IP']) && !empty($_SERVER['YIDIAN_LOCAL_IP'])){
$ip = $_SERVER['YIDIAN_LOCAL_IP'];
}
$handle = stream_socket_client("udp://{$ip}:{$port}", $errno,$errstr, 1);
if($handle){
//在一个stream上设置超时
stream_set_timeout($handle,0,15000);
fwrite($handle, $send_msg."\n");
//从封装协议文件指针中取得报头/元数据,返回数组,索引timed_out为超时信息,超时则为true,未超时则为false
$info = stream_get_meta_data($handle);
fclose($handle);
if ($info['timed_out']) {
return 'fwrite timeout!';
} else {
return $send_msg;
}
}else{
return "stream_socket_client timeout ($errno):$errstr";
}
}
/**
* 针对第三方服务接入,返回状态码与请求响应总时间进行监控打点
*
* @param string $module 请求模块
* @param string $url 请求第三方服务url
* @param int $code 请求返回状态码
* @param int $request_time 请求总时长
* @return string 打点信息或""
*/
public static function proxyMon($module, $url, $code, $request_time = -899){
$result = "";
if(is_string($url) && !empty($url)){
//截取请求第三方服务的uri
$index = strpos($url,'?');
if($index != 0){
$preg_uri = substr($url,0, $index);
}else{
$preg_uri = $url;
}
//替换$uri中的"."为"_", ":"为"_", "/"为"-"
$request_uri = str_replace(array(".", ":", "/"),array("_", "_", "-"), $preg_uri);
//接口返回状态码打点
if(!empty($code) && is_numeric($code)){
$result = MonUtil::counting($module . "." . strval($request_uri), strval($code),1) . "\n";
}else{
$result = MonUtil::counting($module . "." . strval($request_uri), strval(-999),1) . "\n";
}
//接口请求响应总时间打点
if(!empty($request_time) && is_numeric($request_time) && $request_time != -899){
$result = $result . MonUtil::timing(strval($request_uri),"TotalTime", $request_time);
}
}
return $result;
}
/**
* @param string $module
* @param string $index
* @param int $value
* @param string $type 监控打点类型
* @return string 返回监控打点信息
*/
public static function getMsg($module, $index, $value, $type){
if(is_string($module) && !empty($module) && is_string($index) && !empty($index)){
return "Ydbot.statsd.superfe.api." . $module . "." . $index . ".1sec:" . $value . "|" . $type;
}
return "";
}
/**
* 对值进行计数。在一个flush区间,把上报的值累加;flush输出的时候,将上报的值重新清零
*
* @param string $module 子模块名字
* @param string $index 指标
* @param int $value
* @return mixed
*/
public static function counting($module = 'api', $index = '', $value = 0){
$result = "";
$send_msg = self::getMsg($module, $index, $value, "c");
if($send_msg){
$result = self::base($send_msg);
}
return $result;
}
/**
* 标量是任何可以测量的一维变量,可以通过显示的加入符号让系统进行值的累加,不加符号时添加的新值会覆盖旧值
*
* @param string $module 子模块名字
* @param string $index 指标
* @param int $value
* @return mixed
*/
public static function gauges($module = 'api', $index = '', $value = 0){
$result = "";
$send_msg = self::getMsg($module, $index, $value, "g");
if($send_msg){
$result = self::base($send_msg);
}
return $result;
}
/**
* 记录flush期间,不重复的值。譬如对于一个接口,记录有多少个user访问
*
* @param string $module 子模块名字
* @param string $index 指标
* @param int $value
* @return mixed
*/
public static function sets($module = 'api', $index = '', $value = 0){
$result = "";
$send_msg = self::getMsg($module, $index, $value, "s");
if($send_msg){
$result = self::base($send_msg);
}
return $result;
}
/**
* 记录某个操作的耗时,单位ms。在一个flush期间,会自动记录平均值(mean)、最大值(upper)、最小值(lower)、
* 个数(count)以及部分百分值(P95/P99/P999)
*
* @param string $module 子模块名字
* @param string $index 指标
* @param int $value
* @return mixed
*/
public static function timing($module = 'api', $index = '', $value = 0){
$result = "";
$send_msg = self::getMsg($module, $index, $value, "ms");
if($send_msg){
$result = self::base($send_msg);
}
return $result;
}
/**
* 记录指标概率,Value为“100”和“0”。在一个flush期间,成功的概率
*
* @param string $module 子模块名字
* @param string $index 指标
* @param int $value
* @return mixed
*/
public static function rate($module = 'api', $index = '', $value = 0){
$result = "";
$send_msg = self::getMsg($module, $index, $value, "rate");
if($send_msg){
$result = self::base($send_msg);
}
return $result;
}
}
<?php
namespace Api\PhpUtils\Mongo;
use MongoDB\Client;
use MongoDB\Driver\Manager;
use MongoDB\Driver\ReadConcern;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern;
/**
* MongoDB基类
*
* 1. MongoDB配置信息在yaconf的mongo.ini中,configIndex, databaseName, collectionName 在子类中设置
*
* 2. writeConcern, readConcern, readPreference, typeMap 是可以从MongoDB\Driver\Manager继承的,
* 可以参考扩展API文档修改
* $options = [
* 'readConcern' => new ReadConcern(ReadConcern::LOCAL),
* 'readPreference' => new ReadPreference(ReadPreference::RP_SECONDARY_PREFERRED),
* 'typeMap' => ['root' => 'array'],
* 'writeConcern' => new WriteConcern(WriteConcern::MAJORITY),
* ];
* @see https://www.php.net/manual/zh/book.mongodb.php
*
* 3. 提供常用的CURD方法,共他方法会调用 __call()方法
*
* 4. MongoDB建议对重要的数据使用 {w: “marjority”} 的选项 (WriteConcern::MAJORITY)。
* 可以保证数据在复制到多数节点后才返回成功结果。使用该机制可以有效防止数据回滚的发生。
* 另外你可以使用 {j:1} (可以和 w:”majrotiy” 结合使用)来指定数据必须在写入WAL日志之后才向应用返回成功确认。这个会导致写入性能有所下降,但是对于重要的数据可以考虑使用
*
* 5. 异常的捕获与处理 及 getMatchedCount(), getModifiedCount(), getUpsertedCount() 在业务中按需获取,写入retry(1s,2s,4s...)
*
* 6. 根据镜像中php的monogodb扩展版本选择使用1.4版本
* @see https://docs.mongodb.com/drivers/php/ 驱动兼容对照表
* @see https://docs.mongodb.com/php-library/v1.4/ 官方文档
*/
abstract class MongoBase
{
private static $instance = null;
private $client = null;
private $database = null;
private $collection = null;
const W = 1; // 要求进行写入确认
const WTIMEOUTMS = 1000; // 写入关注超时时间
const MAXTIMEMS = 100; //最大执行时间,毫秒
/**
* 获取子类实例
* @return object
*/
public static function getInstance()
{
if (empty(self::$instance)) {
self::$instance = new static();
}
return self::$instance;
}
/**
* 根据配置,构建实例
*/
final private function __construct()
{
$conf = config('mongo', $this->getConfigIndex());
if (!empty($conf)) {
$uriOptions = [];
$driverOptions = [];
if (isset($conf['uriOptions'])) {
$uriOptions = $conf['uriOptions'];
//某些配置项值必须是数字
foreach ($uriOptions as $key => $value) {
$keys = ['connectTimeoutMS', 'socketTimeoutMS', 'wTimeoutMS', 'w'];
if (in_array($key, $keys)) {
$uriOptions[$key] = intval($value);
}
}
}
if (isset($conf['driverOptions'])) {
$driverOptions = $conf['driverOptions'];
}
//初始化客户端
//兼容多个复制集的情况(rs_interact1.mongo.int.yidian-inc.com:27017,rs_interact2.mongo.int.yidian-inc.com:27017,rs_interact3.mongo.int.yidian-inc.com:27017)
if (strpos($conf['host'], ':') !== false) {
$this->client = new Client(
"mongodb://{$conf['host']}",
$uriOptions,
$driverOptions
);
}else{
$this->client = new Client(
"mongodb://{$conf['host']}:{$conf['port']}",
$uriOptions,
$driverOptions
);
}
$this->database = $this->client->selectDatabase($this->getDatabaseName());
$this->collection = $this->database->selectCollection($this->getCollectionName(), $this->getCollectionOptions());
}
}
/**
* 不允许clone
*
* @return void
*/
private function __clone(){}
/**
* 允许调用Libery自己的方法
*
* @param string $method Missing method name
* @param array $args Method arguments
*
* @return mixed
* @throws UnsupportedException if options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function __call($method, array $args)
{
if (!$this->collection) {
return false;
}
return call_user_func_array([$this->collection, $method], $args);
}
/**
* 获取mongode配置,由子类实现
* @return string
*/
abstract protected function getConfigIndex();
/**
* 获取数据库名,由子类实现
* @return string
*/
abstract protected function getDatabaseName();
/**
* 获取表名,由子类实现
* @return string
*/
abstract protected function getCollectionName();
/**
* Get Client instance
*
* @return object
*/
public function getClient()
{
return $this->client;
}
/**
* Get Database instance
*
* @return object
*/
public function getDatabase()
{
return $this->database;
}
/**
* Get Collection instance
*
* @return object
*/
public function getCollection()
{
return $this->collection;
}
/**
* 集合的初始化参数
*
* 可以继承初始化配置文件中,默认的,或都子类中的
*
* @param array $collectionOptions
* @return array
*/
protected function getCollectionOptions(array $collectionOptions = [])
{
$collectionOptions += [
'readConcern' => $this->client->getreadConcern() ?: new ReadConcern(ReadConcern::LOCAL), //3.2之前的服务器版本不支持此功能
'readPreference' => $this->client->getReadPreference() ?: new ReadPreference(ReadPreference::RP_SECONDARY_PREFERRED),
'typeMap' => ['root' => 'array', 'document' => 'array'],
'writeConcern' => $this->client->getWriteConcern() ?: new WriteConcern(self::W, 3 * self::WTIMEOUTMS),
];
return $collectionOptions;
}
/**
* Inserts one document.
*
* @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-insertOne/
* @param array|object $document The document to insert
* @param array $options Command options
* @return InsertOneResult
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function insertOne($document, array $options = [])
{
$options += [
'writeConcern' => new WriteConcern(self::W, 3 * self::WTIMEOUTMS), // W=1要求进行写入确认, 超时时间3000毫秒
];
return $this->collection->insertOne($document, $options);
}
/**
* Inserts multiple documents.
*
* @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-insertMany/
* @param array[]|object[] $documents The documents to insert
* @param array $options Command options
* @return InsertManyResult
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function insertMany(array $documents, array $options = [])
{
$options += [
'ordered' => true, // 默认值为true, 当单个写入失败时,该操作将停止而不执行剩余的写入操作并引发异常
'writeConcern' => new WriteConcern(self::W, 5 * self::WTIMEOUTMS),
];
return $this->collection->insertMany($documents, $options);
}
/**
* Deletes at most one document matching the filter.
*
* @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-deleteOne/
* @param array|object $filter Query by which to delete documents
* @param array $options Command options
* @return DeleteResult
* @throws UnsupportedException if options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function deleteOne($filter, array $options = [])
{
$options += [
'writeConcern' => new WriteConcern(self::W, 3 * self::WTIMEOUTMS),
];
return $this->collection->deleteOne($filter, $options);
}
/**
* Deletes all documents matching the filter.
*
* @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-deleteMany/
* @param array|object $filter Query by which to delete documents
* @param array $options Command options
* @return DeleteResult
* @throws UnsupportedException if options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function deleteMany($filter, array $options = [])
{
$options += [
'writeConcern' => new WriteConcern(self::W, 5 * self::WTIMEOUTMS), // W=1要求进行写入确认, 超时时间3000毫秒
];
return $this->collection->deleteMany($filter, $options);
}
/**
* Finds a single document matching the query.
*
* @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-findOne/
* @param array|object $filter Query by which to filter documents
* @param array $options Additional options
* @return array|object|null
* @throws UnsupportedException if options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function findOne($filter = [], array $options = [])
{
$options += [
'maxTimeMS' => 3 * self::MAXTIMEMS,
];
return $this->collection->findOne($filter, $options);
}
/**
* Finds documents matching the query.
*
* @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-find/
* @param array|object $filter Query by which to filter documents
* @param array $options Additional options
* @return array
* @throws UnsupportedException if options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function find($filter = [], array $options = [])
{
$options += [
'limit' => 1000,
'maxTimeMS' => 10 * self::MAXTIMEMS,
];
$cursor = $this->collection->find($filter, $options);
return $cursor->toArray();
}
/**
* Finds a single document and updates it, returning either the original or
* the updated document.
*
* The document to return may be null if no document matched the filter. By
* default, the original document is returned. Specify
* FindOneAndUpdate::RETURN_DOCUMENT_AFTER for the "returnDocument" option
* to return the updated document.
*
* @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-findOneAndDelete/
* @param array|object $filter Query by which to filter documents
* @param array|object $update Update to apply to the matched document
* @param array $options Command options
* @return array|object|null
* @throws UnexpectedValueException if the command response was malformed
* @throws UnsupportedException if options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function findOneAndUpdate($filter, $update, array $options = [])
{
$options += [
'upsert' => true, // 如果设置为true,则在没有文档符合查询条件时创建一个新文档
'maxTimeMS' => 5 * self::MAXTIMEMS,
'returnDocument' => \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_BEFORE, //返回更新之前文档
];
return $this->collection->findOneAndUpdate($filter, $update, $options);
}
/**
* Finds a single document and deletes it, returning the original.
*
* The document to return may be null if no document matched the filter.
*
* @see https://docs.mongodb.com/php-library/v1.4/reference/method/MongoDBCollection-findOneAndDelete/
* @param array|object $filter Query by which to filter documents
* @param array $options Command options
* @return array|object|null
* @throws UnexpectedValueException if the command response was malformed
* @throws UnsupportedException if options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function findOneAndDelete($filter, array $options = [])
{
$options += [
'maxTimeMS' => 5 * self::MAXTIMEMS,
];
return $this->collection->findOneAndDelete($filter, $options);
}
/**
* Updates at most one document matching the filter.
*
* @see http://docs.mongodb.org/manual/reference/command/update/
* @param array|object $filter Query by which to filter documents
* @param array|object $update Update to apply to the matched document
* @param array $options Command options
* @return UpdateResult
* @throws UnsupportedException if options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function updateOne($filter, $update, array $options = [])
{
$options += [
'upsert' => true, // 如果设置为true,则在没有文档符合查询条件时创建一个新文档
];
return $this->collection->updateOne($filter, $update, $options);
}
/**
* Updates all documents matching the filter.
*
* @see http://docs.mongodb.org/manual/reference/command/update/
* @param array|object $filter Query by which to filter documents
* @param array|object $update Update to apply to the matched documents
* @param array $options Command options
* @return UpdateResult
* @throws UnsupportedException if options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function updateMany($filter, $update, array $options = [])
{
$options += [
'upsert' => true, // 如果设置为true,则在没有文档符合查询条件时创建一个新文档
];
return $this->collection->updateMany($filter, $update, $options);
}
/**
* Gets the number of documents matching the filter.
*
* @param array|object $filter Query by which to filter documents
* @param array $options Command options
* @return integer
* @throws UnexpectedValueException if the command response was malformed
* @throws UnsupportedException if options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function countDocuments($filter = [], array $options = [])
{
$options += [
'maxTimeMS' => 10 * self::MAXTIMEMS, // count操作尽量少做
];
return $this->collection->countDocuments($filter, $options);
}
}
## Mongo工具库
### 请使用MongoBase类,原有的Base类将废弃
## Mongo工具库使用示例
``` php
namespace App\Models\demo\mongo;
use Api\PhpUtils\Mongo\MongoBase;
class Merchant extends MongoBase
{
protected function getConfigIndex()
{
return "merchant";
}
protected function getDatabaseName()
{
return "merchant";
}
protected function getCollectionName()
{
return "test";
}
protected function getWhere($params)
{
return $params;
}
}
```
<?php
namespace Api\PhpUtils\Mysql;
use Medoo\Medoo;
use Api\PhpUtils\Log\FileLog;
/**
* MySQL基类,支持一主多从
*
* 1. MySQL配置信息在yaconf的mysql.DATABASE_NAME, TABLE_NAME, CONFIG_INDEX 在子类中设置
*
* 2. 数据库分库分表,需要发号器全局唯一id,请用MysqlClusterBase基类
*
* 3. 提供常用的CURD方法,共他方法会调用 __callStatic()方法
*
* 4. 返回值说明:
* return false 是校验参数错误,数据库执行错误, debug模式等异常的标识,错误信息请看日志,
* 使用error()方法可以获取最后的一条错误, 对错误进行了一次强制重试,
* return [] 查询结果为空
* return number 插入为lasterInsertId:String 按需自己intval(),更新和删除返回影响条数
*
* 5. 根据镜像中php的版本选择使用version1.7.10 版本的Medoo类,不要升级!
* @see https://medoo.in (英文网站)
* @see https://medoo.lvtao.net (中文网站)
*/
/**
* @method static MysqlBase insert($data)
* @method static MysqlBase update($data, $where)
* @method static MysqlBase delete($where)
* @method static MysqlBase select($columns, $where)
* @method static MysqlBase selectMaster($columns, $where)
* @method static MysqlBase getMaster($columns, $where)
* @method static MysqlBase get($columns, $where)
* @method static MysqlBase count($where)
* @method static MysqlBase action( $callback )
*/
abstract class MysqlBase
{
const DATABASE_TYPE = 'mysql'; //数据库类型
const TABLE_NAME = ''; //表名,必填
const CONFIG_INDEX = ''; //yaconf中的index索引,必填
const WRITE = 'write';
const READ = 'read';
const PRIMARY_KEY = 'id';
/**
* yaconf中的数据库配置项
*
* @var array
*/
private static $iniConf = [];
/**
* 数据库配置
*
* @var array
*/
private static $dbConf = [];
/**
* 数据库连接信息,分主从
*
* @var array
*/
private static $dbConnect = [];
/**
* 数据库当前连接信息
*
* @var Medoo\Medoo
*/
private static $dbCurrentConnect = null;
/**
* 数据库查询等的返回结果
*
* @var mixed
*/
private static $sqlResut = [];
/**
* 数据库最后一个
*
* @var array
*/
private static $errorInfo = [];
/**
* 写操作函数
*
* @var array
*/
protected static $writeFuncitonList = [
"insert",
"update",
"delete",
"selectMaster", // 或使用/*master*/强制路由,对主从实时性要求较高的场景使用,分布式系统尽量用消息代替查主库等其他方案
"getMaster",
];
/**
* 读操作函数
*
* @var array
*/
protected static $readFunctionList = [
"select",
"get",
"count",
];
public static function __callStatic($method, $arguments)
{
if (!self::validation($method, $arguments)) {
return false;
}
if (in_array($method, self::$writeFuncitonList)) {
// 写操作
$type = static::WRITE;
self::$dbConnect[$type] = self::getConnection($type);
if (strpos($method, 'Master') !== false) {
$method = substr_replace($method, '', -6);
}
array_unshift($arguments, static::TABLE_NAME);
} elseif (in_array($method, self::$readFunctionList)) {
// 读操作
$type = static::READ;
self::$dbConnect[$type] = self::getConnection($type);
array_unshift($arguments, static::TABLE_NAME);
} elseif (strpos($method, 'query') !== false) {
// Medoo原生SQL操作
$type = static::READ;
if (strpos($method, 'Master') !== false) {
$type = static::WRITE;
}
$method = 'query';
self::$dbConnect[$type] = self::getConnection($type);
} else {
throw new \Exception("use undefined Medoo function:" . $method . "-" . json_encode($arguments));
}
self::$dbCurrentConnect = self::$dbConnect[$type];
self::$sqlResut = call_user_func_array([self::$dbConnect[$type], $method], $arguments);
if (!self::catchError($method, $arguments)) {
// 如果失败重试一次
self::$dbCurrentConnect = self::$dbConnect[$type] = self::getConnection($type);
self::$sqlResut = call_user_func_array([self::$dbConnect[$type], $method], $arguments);
if (!self::catchError($method, $arguments)) {
return false;
}
}
return self::formatResult($method, $arguments);
}
/**
* 获取数据库连接
*
* @param [string] $type 读或写
* @param boolean $flush 是否强制重新连接,重试等场景使用
* @return void
*/
public static function getConnection($type, $flush = false)
{
if (!isset(self::$dbConnect[$type]) || $flush) {
self::$dbConnect[$type] = new Medoo(self::getDbConf($type));
}
return self::$dbConnect[$type];
}
/**
* 获取数据库配置
*
* @param string $type 读或写
* @return array
*/
public static function getDbConf($type)
{
self::$iniConf = config('mysql', static::CONFIG_INDEX);
if (!self::$iniConf) {
throw new \Exception('mysql yaconf ini config not exist');
}
list($server, $port) = self::getDbServerPort($type);
$options = self::getDbOptions();
self::$dbConf[$type] = [
'database_type' => static::DATABASE_TYPE ? static::DATABASE_TYPE : self::$iniConf['database_type'],
'database_name' => self::$iniConf['database_name'],
'server' => $server,
'port' => $port,
'username' => self::$iniConf['username'],
'password' => self::$iniConf['password'],
'charset' => self::$iniConf['charset'],
'option' => array_replace([
\PDO::ATTR_STRINGIFY_FETCHES => false,
\PDO::ATTR_EMULATE_PREPARES => false,
\PDO::ATTR_CASE => \PDO::CASE_NATURAL,
\PDO::ATTR_TIMEOUT => intval(self::$iniConf['connect_timeout'] ?? 10)
], $options)
];
return self::$dbConf[$type];
}
/**
* 获取数据库server和port
*
* @param [string] $type 读或写类型
* @return void
*/
public static function getDbServerPort($type)
{
$server = '';
$port = 0;
$host = self::$iniConf[$type]['host'];
// 处理分为的逻辑
if ($type === static::WRITE) {
list($server, $port) = explode(':', $host);
} elseif ($type === static::READ) {
if (strpos($host, ',') === false) {
list($server, $port) = explode(':', $host);
} else {
$list = explode(',', $host);
$rand = array_rand($list);
list($server, $port) = explode(':', $list[$rand]);
}
}
return [$server, $port];
}
/**
* 获取ini中的option配置,必须用\PDO::ATTR_STRINGIFY_FETCHES对应的int数值
*
* @return array
*/
public static function getDbOptions()
{
$options = [];
if (isset(self::$iniConf['options'])) {
foreach (self::$iniConf['options'] as $item) {
$itemArr = explode('=>', $item);
$options[trim($itemArr[0])] = $itemArr[1];
}
}
return $options;
}
/**
* 执行SQL前,先做基础的验证
*/
private static function validation($method, &$arguments)
{
switch (strtolower($method)) {
case 'update':
// 更新操作前,如果没有where条件
if (!isset($arguments[1]) || empty($arguments[1])) {
self::$errorInfo = ['validation', 0, "sql exception write without where:" . $method . "-" . json_encode($arguments)];
FileLog::error("sql exception write without where:" . $method . "-" . json_encode($arguments));
return false;
}
break;
case 'delete':
// 删除操作前,如果没有where条件
if (!isset($arguments[0]) || empty($arguments[0])) {
self::$errorInfo = ['validation', 0, "sql exception write without where:" . $method . "-" . json_encode($arguments)];
FileLog::error("sql exception write without where:" . $method . "-" . json_encode($arguments));
return false;
}
// 如果必须要物理删除数据,只允计按主键或唯一键单条删除
if (!isset($arguments[0][static::PRIMARY_KEY])) {
self::$errorInfo = ['validation', 0, "sql exception write error primary key:" . $method . "-" . json_encode($arguments)];
FileLog::error("sql exception write error primary key:" . $method . "-" . json_encode($arguments));
return false;
}
break;
case 'select':
case 'selectmaster':
case 'get':
case 'getmaster':
// 不允许用join操作
if (count($arguments) > 2) {
self::$errorInfo = ['validation', 0, "sql exception use join:" . $method . "-" . json_encode($arguments)];
FileLog::error("sql exception use join:" . $method . "-" . json_encode($arguments));
return false;
}
// 读取没有limit的强制加limit
if (!isset($arguments[1]['LIMIT'])) {
$arguments[1]['LIMIT'] = 5000;
return true;
}
break;
case 'count':
// 不允许用join操作
if (count($arguments) > 1) {
self::$errorInfo = ['validation', 0, "sql exception use join:" . $method . "-" . json_encode($arguments)];
FileLog::error("sql exception use join:" . $method . "-" . json_encode($arguments));
return false;
}
break;
default:
break;
}
return true;
}
/**
* 捕获错误
*
* @return bool
*/
private static function catchError($method, $arguments)
{
self::$errorInfo = call_user_func_array([self::$dbCurrentConnect, 'error'], []);
// debug 模式返回值为NULL
if (is_null(self::$errorInfo)) {
return true;
}
list($sqlState, $errorCode, $errorMessage) = self::$errorInfo;
// PDO::errorInfo() the SQLSTATE error code for '00000' (success) and '01000' (success with warning).
if ($sqlState == '00000') {
return true;
}
FileLog::error("mysql error catch:" . $method . "-" . json_encode($arguments) . '-' . print_r([$sqlState, $errorCode, $errorMessage], 1));
//throw Exception
// $msg = "mysql error catch:" . $method . "-" . json_encode($arguments) . '-' . json_encode([$sqlState, $errorCode, $errorMessage]);
// throw new \Exception($msg);
return false;
}
/**
* 格式化返回结果
*
* @return mixed
*/
private static function formatResult($method, $arguments)
{
// debug 模式errorInfo返回值为NULL
if (is_null(self::$errorInfo)) {
return self::$sqlResut;
}
// get未查询到结果返回NULL,select返回空数组[],统一转成空数组[]
if (is_null(self::$sqlResut)) {
return [];
}
switch (strtolower($method)) {
case 'insert':
// 多条数据插入,返回影响的行数
if (isset($arguments[1][1])) {
// PDOStatement::rowCount() 返回上一个由对应的 PDOStatement 对象执行DELETE、 INSERT、或 UPDATE 语句受影响的行数。
return self::$sqlResut->rowCount();
}
// 单条插入返回最后插入行的ID
return call_user_func_array([self::$dbCurrentConnect, 'id'], []);
break;
case 'update':
case 'delete':
// 返回上一个由对应的 PDOStatement 对象执行DELETE、 INSERT、或 UPDATE 语句受影响的行数。
return self::$sqlResut->rowCount();
break;
case 'select':
// 返回数组
return self::$sqlResut;
break;
default:
return self::$sqlResut;
break;
}
}
/**
* 获得最后一个执行的错误
*
* @return array
*/
public static function error()
{
return self::$errorInfo;
}
/**
* 开启Medoo调试模式,只输出执行的SQL语句,但不执行,在最开始调用
*
* @return array
*/
public static function debug()
{
$type = static::WRITE;
self::$dbConnect[$type] = self::getConnection($type);
self::$dbConnect[$type]->debug();
$type = static::READ;
self::$dbConnect[$type] = self::getConnection($type);
self::$dbConnect[$type]->debug();
return [static::WRITE => self::$dbConnect[static::WRITE], static::READ => self::$dbConnect[static::READ]];
}
/**
* 返回所有执行的查询
*
* @return array
*/
public static function log()
{
$ret = [];
$type = static::WRITE;
self::$dbConnect[$type] = self::getConnection($type);
$ret[$type] = self::$dbConnect[$type]->log();
$type = static::READ;
self::$dbConnect[$type] = self::getConnection($type);
$ret[$type] = self::$dbConnect[$type]->log();
return $ret;
}
/**
* 返回最后一条执行的SQL语句
*
* @return array
*/
public static function last()
{
return self::$dbCurrentConnect->last();
}
/**
* 事务操作
*
* @return bool
*/
public static function beginTransaction()
{
$type = static::WRITE;
self::$dbConnect[$type] = self::getConnection($type);
if(!self::$dbConnect[$type]->pdo->inTransaction()) {
return self::$dbConnect[$type]->pdo->beginTransaction();
}
return false;
}
/**
* 事务提交
*
* @return bool
*/
public static function commit()
{
$type = static::WRITE;
self::$dbConnect[$type] = self::getConnection($type);
if(self::$dbConnect[$type]->pdo->inTransaction()) {
return self::$dbConnect[$type]->pdo->commit();
}
return false;
}
/**
* 事务回滚
*
* @return bool
*/
public static function rollback()
{
$type = static::WRITE;
self::$dbConnect[$type] = self::getConnection($type);
if(self::$dbConnect[$type]->pdo->inTransaction()) {
return self::$dbConnect[$type]->pdo->rollback();
}
return false;
}
/**
* 事务操作,Medoo提供的
*
* @return mixed
*/
public static function action(callable $actions)
{
$type = static::WRITE;
self::$dbConnect[$type] = self::getConnection($type);
return self::$dbConnect[$type]->action($actions);
}
}
<?php
namespace Api\PhpUtils\Mysql;
use Medoo\Medoo;
use Api\PhpUtils\Log\FileLog;
/**
* MySQL基类,支持分库分表
*
* 1. MySQL配置信息在yaconf的mysql. DATABASE_NAME, TABLE_NAME, CONFIG_INDEX 在子类中设置
*
* 2. 数据库分库分表,需要发号器全局唯一id,不需要分库分表请使用MysqlBase基类
*
* 3. 提供常用的CURD方法,共他方法会调用 __callStatic()方法
*
* 4. 返回值说明:
* return false 是校验参数错误,数据库执行错误, debug模式等异常的标识,错误信息请看日志,
* 使用error()方法可以获取最后的一条错误, 对错误进行了一次强制重试,
* return [] 查询结果为空
* return number 插入为lasterInsertId:String 按需自己intval(),更新和删除返回影响条数
*
* 5. 根据镜像中php的版本选择使用version1.7.10 版本的Medoo类,不要升级!
* @see https://medoo.in (英文网站)
* @see https://medoo.lvtao.net (中文网站)
*/
abstract class MysqlClusterBase
{
const DATABASE_TYPE = 'mysql'; //数据库类型
const TABLE_NAME = ''; //表名,必填
const DATABASE_NAME = ''; //数据库名称,必填
const CONFIG_INDEX = ''; //yaconf中的index索引,必填
const SHARDING_DATABASE = true; // 库分片标识
const SHARDING_TABLE = false; // 表分片标识
const SHARDING_DATABASE_COLUMN = ''; // 数据库分片字段order_id
const SHARDING_DATABASE_ALGORITHM = ''; // 数据库分片算法策略mod(-6,-5)
const SHARDING_DATABASE_COMPAT_COLUMN = ''; // 兼容数据库分片字段user_id
const SHARDING_DATABASE_COMPAT_ALGORITHM = ''; // 兼容数据库分片算法表达式mod(-2,-1)
const SHARDING_TABLE_COLUMN = ''; // 表分片字段order_id
const SHARDING_TABLE_ALGORITHM = ''; // 表分片算法表达式DateM(0,-6)
// const SHARDING_TABLE_COMPAT_COLUMN = ''; // 兼容表分片字段user_id
// const SHARDING_TABLE_COMPAT_ALGORITHM = ''; // 兼容表分片算法表达式DateM(0,-6)
const WRITE = 'write';
const READ = 'read';
const PRIMARY_KEY = 'id';
/**
* yaconf中的数据库配置项
*
* @var array
*/
private static $iniConf = [];
/**
* 数据库配置
*
* @var array
*/
private static $dbConf = [];
/**
* 数据库连接信息,分主从
*
* @var array
*/
private static $dbConnect = [];
/**
* 数据库当前连接信息
*
* @var Medoo\Medoo
*/
private static $dbCurrentConnect = null;
/**
* 数据库当前事务索引
*
* @var integer
*/
private static $transationIndex = 0;
/**
* 数据库分片数
*
* @var integer
*/
private static $dbShardingCount = 1;
/**
* 数据库分片列对应值
*
* @var integer
*/
private static $dbShardingColumnValue = 0;
/**
* 数据库分片兼容列对应值
*
* @var integer
*/
private static $dbShardingCompatColumnValue = 0;
/**
* 数据库查询等的返回结果
*
* @var mixed
*/
private static $sqlResut = [];
/**
* 数据库最后一个
*
* @var array
*/
private static $errorInfo = [];
/**
* 写操作函数
*
* @var array
*/
protected static $writeFuncitonList = [
"insert",
"update",
"delete",
"selectMaster", // 或使用/*master*/强制路由,对主从实时性要求较高的场景使用,分布式系统尽量用消息代替查主库等其他方案
"getMaster",
];
/**
* 读操作函数
*
* @var array
*/
protected static $readFunctionList = [
"select",
"get",
"count",
];
public static function __callStatic($method, $arguments)
{
// 执行SQL前,先做基础的验证
if (!self::validation($method, $arguments)) {
return false;
}
// 解析参数中数据库分片值
if (!self::parseDatabaseShardingColumnValue($method, $arguments)) {
return false;
}
// 读取配置文件内容
self::$iniConf = config('mysql', static::CONFIG_INDEX);
if (!self::$iniConf) {
throw new \Exception('mysql config not exist');
}
// 获取库分片索引
$dbShardingIndex = self::getDatabaseShardingIndex();
if (in_array($method, self::$writeFuncitonList)) {
// 写操作
$type = static::WRITE;
self::$dbConnect[$type.':'.$dbShardingIndex] = self::getConnection($type, $dbShardingIndex);
if (strpos($method, 'Master') !== false) {
$method = substr_replace($method, '', -6);
}
array_unshift($arguments, static::TABLE_NAME);
} elseif (in_array($method, self::$readFunctionList)) {
// 读操作
$type = static::READ;
self::$dbConnect[$type.':'.$dbShardingIndex] = self::getConnection($type, $dbShardingIndex);
array_unshift($arguments, static::TABLE_NAME);
} else {
throw new \Exception("use undefined Medoo function:" . $method . "-" . json_encode($arguments));
}
self::$dbCurrentConnect = self::$dbConnect[$type.':'.$dbShardingIndex];
self::$sqlResut = call_user_func_array([self::$dbConnect[$type.':'.$dbShardingIndex], $method], $arguments);
if (!self::catchError($method, $arguments)) {
// 如果失败重试一次
self::$dbCurrentConnect = self::$dbConnect[$type.':'.$dbShardingIndex] = self::getConnection($type, $dbShardingIndex, true);
self::$sqlResut = call_user_func_array([self::$dbConnect[$type.':'.$dbShardingIndex], $method], $arguments);
if (!self::catchError($method, $arguments)) {
return false;
}
}
return self::formatResult($method, $arguments);
}
/**
* 获取数据库连接
*
* @param [string] $type 读或写
* @param boolean $flush 是否强制重新连接,重试等场景使用
* @return void
*/
public static function getConnection($type, $dbShardingIndex, $flush = false)
{
if (!isset(self::$dbConnect[$type.':'.$dbShardingIndex]) || $flush) {
self::$dbConnect[$type.':'.$dbShardingIndex] = new Medoo(self::getDbConf($type, $dbShardingIndex));
}
return self::$dbConnect[$type.':'.$dbShardingIndex];
}
/**
* 获取数据库配置
*
* @param string $type 读或写
* @return array
*/
public static function getDbConf($type, $dbShardingIndex)
{
list($server, $port) = self::getDbServerPort($type, $dbShardingIndex);
$options = self::getDbOptions();
self::$dbConf[$server . ':' . $port] = [
'database_type' => static::DATABASE_TYPE ? static::DATABASE_TYPE : self::$iniConf['database_type'],
'database_name' => self::$iniConf['database_name'],
'server' => $server,
'port' => $port,
'username' => self::$iniConf['username'],
'password' => self::$iniConf['password'],
'charset' => self::$iniConf['charset'],
'option' => array_replace([
\PDO::ATTR_STRINGIFY_FETCHES => false,
\PDO::ATTR_EMULATE_PREPARES => false,
\PDO::ATTR_CASE => \PDO::CASE_NATURAL,
\PDO::ATTR_TIMEOUT => intval(self::$iniConf['connect_timeout'] ?? 10)
], $options)
];
return self::$dbConf[$server . ':' . $port];
}
/**
* 获取数据库server和port
*
* @param string $type 读或写类型
* @return void
*/
private static function getDbServerPort($type, $dbShardingIndex)
{
$server = '';
$port = 0;
$host = self::$iniConf[$type]['host'];
// 处理分为写读逻辑
if ($type === static::WRITE) {
if (!static::SHARDING_DATABASE || strpos($host, ',') === false) {
// 主库未分片,但表分片了
list($server, $port) = explode(':', $host);
} else {
// 主库已分片
$hostList = explode(',', $host);
// self::$dbShardingCount = count($hostList);
// $dbShardingIndex = self::getDatabaseShardingIndex();
$sharddingHost = $hostList[$dbShardingIndex];
list($server, $port) = explode(':', $sharddingHost);
}
} elseif ($type === static::READ) {
if (!static::SHARDING_DATABASE || count($host) === 1) {
// 从库未分片,但表分片了
if (strpos($host, ',') === false) {
list($server, $port) = explode(':', $host);
} else {
$list = explode(',', $host);
$rand = array_rand($list);
list($server, $port) = explode(':', $list[$rand]);
}
} else {
// 从库已分片,根据配置选择分片策略
// self::$dbShardingCount = count($host);
// $dbShardingIndex = self::getDatabaseShardingIndex();
$slaveHost = $host[$dbShardingIndex];
if (strpos($slaveHost, ',') === false) {
list($server, $port) = explode(':', $slaveHost);
} else {
$list = explode(',', $slaveHost);
$rand = array_rand($list);
list($server, $port) = explode(':', $list[$rand]);
}
}
}
return [$server, $port];
}
/**
* 获取数据库分片数量
*
* @return integer
*/
private static function getDbShardingCount() {
$host = self::$iniConf[static::WRITE]['host'];
$hostList = explode(',', $host);
self::$dbShardingCount = count($hostList);
}
/**
* 获取数据库分片策略
*
* @return array
*/
private static function getDatabaseShardingIndex()
{
// 未使用分库
if (!static::SHARDING_DATABASE) {
return 0;
}
self::getDbShardingCount();
$dbShardingIndex = 0;
if (self::$dbShardingColumnValue) {
$modKey = self::$dbShardingColumnValue;
if (static::SHARDING_DATABASE_COLUMN && static::SHARDING_DATABASE_ALGORITHM) {
// 取模的分库策略
if (stripos(static::SHARDING_DATABASE_ALGORITHM, 'mod') !== false) {
// 需要根据SHARDING_DATABASE_COLUMN配置字段截取字段
if (strpos(static::SHARDING_DATABASE_ALGORITHM, '(') !== false) {
$startEndString = substr(static::SHARDING_DATABASE_ALGORITHM, strpos(static::SHARDING_DATABASE_ALGORITHM, '(') + 1, -1);
if (strpos($startEndString, ',') !== false) {
// 有start,end
list($start, $end) = explode(',', $startEndString);
$modKey = substr(self::$dbShardingColumnValue, $start, $end);
} else {
// 只有start,无end
$modKey = substr(self::$dbShardingColumnValue, $startEndString);
}
}
// 计算mod取模后的值
$dbShardingIndex = intval($modKey) % self::$dbShardingCount;
return $dbShardingIndex;
}
}
}elseif(self::$dbShardingCompatColumnValue) {
$modKey = self::$dbShardingCompatColumnValue;
if (static::SHARDING_DATABASE_COMPAT_COLUMN && static::SHARDING_DATABASE_COMPAT_ALGORITHM) {
// 取模的分库策略
if (stripos(static::SHARDING_DATABASE_ALGORITHM, 'mod') !== false) {
// 需要根据SHARDING_DATABASE_COMPAT_COLUMN配置字段截取字段
if (strpos(static::SHARDING_DATABASE_ALGORITHM, '(') !== false) {
$startEndString = substr(static::SHARDING_DATABASE_ALGORITHM, strpos(static::SHARDING_DATABASE_ALGORITHM, '(') + 1, -1);
if (strpos($startEndString, ',') !== false) {
// 有start,end
list($start, $end) = explode(',', $startEndString);
$modKey = substr(self::$dbShardingCompatColumnValue, $start, $end);
} else {
// 只有start,无end
$modKey = substr(self::$dbShardingCompatColumnValue, $startEndString);
}
}
// 计算mod取模后的值
$dbShardingIndex = intval($modKey) % self::$dbShardingCount;
return $dbShardingIndex;
}
}
}
// echo 'exp';
throw new \Exception('mysql not get sharding database-stragegy');
}
/**
* 获取ini中的option配置,必须用\PDO::ATTR_STRINGIFY_FETCHES对应的int数值
*
* @return array
*/
public static function getDbOptions()
{
$options = [];
if (isset(self::$iniConf['options'])) {
foreach (self::$iniConf['options'] as $item) {
$itemArr = explode('=>', $item);
$options[trim($itemArr[0])] = $itemArr[1];
}
}
return $options;
}
/**
* 执行SQL前,先做基础的验证
*/
private static function validation($method, &$arguments)
{
// 没有使用分片方式
if (empty(static::SHARDING_DATABASE_COLUMN) && empty(static::SHARDING_TABLE_COLUMN)) {
echo 'validation1';
self::$errorInfo = ['validation', 0, "sql exception not use sharding mode :" . $method . "-" . json_encode($arguments)];
FileLog::error("sql exception not use sharding mode:" . $method . "-" . json_encode($arguments));
return false;
}
switch (strtolower($method)) {
case 'update':
// 更新操作前,如果没有where条件
if (!isset($arguments[1]) || empty($arguments[1])) {
self::$errorInfo = ['validation', 0, "sql exception write without where:" . $method . "-" . json_encode($arguments)];
FileLog::error("sql exception write without where:" . $method . "-" . json_encode($arguments));
return false;
}
break;
case 'delete':
// 删除操作前,如果没有where条件
if (!isset($arguments[0]) || empty($arguments[0])) {
self::$errorInfo = ['validation', 0, "sql exception write without where:" . $method . "-" . json_encode($arguments)];
FileLog::error("sql exception write without where:" . $method . "-" . json_encode($arguments));
return false;
}
// 如果必须要物理删除数据,只允计按主键或唯一键单条删除
if (!isset($arguments[0][static::PRIMARY_KEY])) {
self::$errorInfo = ['validation', 0, "sql exception write error primary key:" . $method . "-" . json_encode($arguments)];
FileLog::error("sql exception write error primary key:" . $method . "-" . json_encode($arguments));
return false;
}
break;
case 'select':
case 'selectmaster':
case 'get':
case 'getmaster':
// 不允许用join操作
if (count($arguments) > 2) {
self::$errorInfo = ['validation', 0, "sql exception use join:" . $method . "-" . json_encode($arguments)];
FileLog::error("sql exception use join:" . $method . "-" . json_encode($arguments));
return false;
}
// 读取没有limit的强制加limit
if (!isset($arguments[1]['LIMIT'])) {
$arguments[1]['LIMIT'] = 5000;
return true;
}
break;
case 'count':
// 不允许用join操作
if (count($arguments) > 1) {
self::$errorInfo = ['validation', 0, "sql exception use join:" . $method . "-" . json_encode($arguments)];
FileLog::error("sql exception use join:" . $method . "-" . json_encode($arguments));
return false;
}
break;
default:
break;
}
return true;
}
/**
* 解析参数中数据库分片值
*
* @param string $method
* @param array $arguments
* @return bool
*/
private static function parseDatabaseShardingColumnValue($method, $arguments)
{
// 未使用分库
if (!static::SHARDING_DATABASE) {
return true;
}
// 数据库分片,需要提取条件中的分片列及分片兼容列,比如order_id及user_id
if (!empty(static::SHARDING_DATABASE_COLUMN) || !empty(static::SHARDING_DATABASE_COMPAT_COLUMN)) {
switch (strtolower($method)) {
case 'update':
case 'select':
case 'selectmaster':
case 'get':
case 'getmaster':
$where = $arguments[1]; //第二个参数
if (isset($where[static::SHARDING_DATABASE_COLUMN])) {
// echo $where[static::SHARDING_DATABASE_COLUMN];exit;
self::$dbShardingColumnValue = $where[static::SHARDING_DATABASE_COLUMN];
} elseif (isset($where[static::SHARDING_DATABASE_COMPAT_COLUMN])) {
self::$dbShardingCompatColumnValue = $where[static::SHARDING_DATABASE_COMPAT_COLUMN];
} elseif (isset($where['AND'][static::SHARDING_DATABASE_COLUMN])) {
// where 条件中只支持AND操作,不要使用OR操作
self::$dbShardingColumnValue = $where['AND'][static::SHARDING_DATABASE_COLUMN];
} elseif (isset($where['AND'][static::SHARDING_DATABASE_COMPAT_COLUMN])) {
self::$dbShardingCompatColumnValue = $where['AND'][static::SHARDING_DATABASE_COMPAT_COLUMN];
}
break;
case 'delete':
case 'count':
$where = $arguments[0] ?? []; //第一个参数
if (isset($where[static::SHARDING_DATABASE_COLUMN])) {
self::$dbShardingColumnValue = $where[static::SHARDING_DATABASE_COLUMN];
} elseif (isset($where[static::SHARDING_DATABASE_COMPAT_COLUMN])) {
self::$dbShardingCompatColumnValue = $where[static::SHARDING_DATABASE_COMPAT_COLUMN];
} elseif (isset($where['AND'][static::SHARDING_DATABASE_COLUMN])) {
self::$dbShardingColumnValue = $where['AND'][static::SHARDING_DATABASE_COLUMN];
} elseif (isset($where['AND'][static::SHARDING_DATABASE_COMPAT_COLUMN])) {
self::$dbShardingCompatColumnValue = $where['AND'][static::SHARDING_DATABASE_COMPAT_COLUMN];
}
break;
case 'insert':
$data = $arguments[0]; // data中的值
// 单条
if (isset($data[static::SHARDING_DATABASE_COLUMN])) {
self::$dbShardingColumnValue = $data[static::SHARDING_DATABASE_COLUMN];
} else {
// 只支持插入多条同库中的数据
$key = array_unique(array_column($data, static::SHARDING_DATABASE_COLUMN));
if ($key && count($key) === 1) {
self::$dbShardingColumnValue = $data[0][static::SHARDING_DATABASE_COLUMN];
} else {
return false; // 暂不支持一次插入多个库中数据
}
}
break;
}
if (self::$dbShardingColumnValue || self::$dbShardingCompatColumnValue) {
return true;
}
self::$errorInfo = ['parseDatabaseShardingColumnValue', 0, "not find sharding column:" . $method . "-" . json_encode($arguments)];
FileLog::error("not find sharding column:" . $method . "-" . json_encode($arguments));
return false;
}
// 使用数据库分片,未指定分片对应列返回false
self::$errorInfo = ['parseDatabaseShardingColumnValue', 0, "not set sharding column:" . $method . "-" . json_encode($arguments)];
FileLog::error("not set sharding column:" . $method . "-" . json_encode($arguments));
return false;
}
/**
* 捕获错误
*
* @return bool
*/
private static function catchError($method, $arguments)
{
self::$errorInfo = call_user_func_array([self::$dbCurrentConnect, 'error'], []);
// debug 模式返回值为NULL
if (is_null(self::$errorInfo)) {
return true;
}
list($sqlState, $errorCode, $errorMessage) = self::$errorInfo;
// PDO::errorInfo() the SQLSTATE error code for '00000' (success) and '01000' (success with warning).
if ($sqlState == '00000') {
return true;
}
FileLog::error("mysql error catch:" . $method . "-" . json_encode($arguments) . '-' . print_r([$sqlState, $errorCode, $errorMessage], 1));
//throw Exception
// $msg = "mysql error catch:" . $method . "-" . json_encode($arguments) . '-' . json_encode([$sqlState, $errorCode, $errorMessage]);
// throw new \Exception($msg);
return false;
}
/**
* 格式化返回结果
*
* @return mixed
*/
private static function formatResult($method, $arguments)
{
// debug 模式errorInfo返回值为NULL
if (is_null(self::$errorInfo)) {
return self::$sqlResut;
}
// get未查询到结果返回NULL,select返回空数组[],统一转成空数组[]
if (is_null(self::$sqlResut)) {
return [];
}
switch (strtolower($method)) {
case 'insert':
// 多条数据插入,返回影响的行数
if (isset($arguments[1][1])) {
// PDOStatement::rowCount() 返回上一个由对应的 PDOStatement 对象执行DELETE、 INSERT、或 UPDATE 语句受影响的行数。
return self::$sqlResut->rowCount();
}
// 单条插入返回最后插入行的ID
return call_user_func_array([self::$dbCurrentConnect, 'id'], []);
break;
case 'update':
case 'delete':
// 返回上一个由对应的 PDOStatement 对象执行DELETE、 INSERT、或 UPDATE 语句受影响的行数。
return self::$sqlResut->rowCount();
break;
case 'select':
// 返回数组
return self::$sqlResut;
break;
default:
return self::$sqlResut;
break;
}
}
/**
* 事务操作(单库事务)
*
* @param int $dbShardingColumnValue
* @return bool
*/
public static function beginTransaction($dbShardingColumnValue = 0, $dbShardingCompatColumnValue = 0)
{
self::$dbShardingColumnValue = $dbShardingColumnValue;
self::$dbShardingCompatColumnValue = $dbShardingCompatColumnValue;
// 读取配置文件内容
self::$iniConf = config('mysql', static::CONFIG_INDEX);
if (!self::$iniConf) {
throw new \Exception('mysql config not exist');
}
// 获取库分片索引
self::$transationIndex = self::getDatabaseShardingIndex();
$type = static::WRITE;
self::$dbConnect[$type.':'.self::$transationIndex] = self::getConnection($type, self::$transationIndex);
if(!self::$dbConnect[$type.':'.self::$transationIndex]->pdo->inTransaction()) {
return self::$dbConnect[$type.':'.self::$transationIndex]->pdo->beginTransaction();
}
return false;
}
/**
* 事务提交
*
* @return bool
*/
public static function commit()
{
$type = static::WRITE;
self::$dbConnect[$type.':'.self::$transationIndex] = self::getConnection($type, self::$transationIndex);
if(self::$dbConnect[$type.':'.self::$transationIndex]->pdo->inTransaction()) {
return self::$dbConnect[$type.':'.self::$transationIndex]->pdo->commit();
}
return false;
}
/**
* 事务回滚
*
* @return bool
*/
public static function rollback()
{
$type = static::WRITE;
self::$dbConnect[$type.':'.self::$transationIndex] = self::getConnection($type, self::$transationIndex);
if(self::$dbConnect[$type.':'.self::$transationIndex]->pdo->inTransaction()) {
return self::$dbConnect[$type.':'.self::$transationIndex]->pdo->rollback();
}
return false;
}
/**
* 事务操作,Medoo提供的
*
* @return mixed
*/
public static function action(callable $actions)
{
$type = static::WRITE;
self::$dbConnect[$type.':'.self::$transationIndex] = self::getConnection($type, self::$transationIndex);
return self::$dbConnect[$type.':'.self::$transationIndex]->action($actions);
}
/**
* 返回最后一条执行的SQL语句
*
* @return array
*/
public static function last()
{
return self::$dbCurrentConnect->last();
}
/**
* 获得最后一个执行的错误
*
* @return array
*/
public static function error()
{
return self::$errorInfo;
}
/**
* 返回所有执行的查询
*
* @return array
*/
public static function log()
{
return self::$dbCurrentConnect->log();
}
/**
* 获取所有分片连接
*
* @return array
*/
public static function getWriteConnectionList()
{
// 读取配置文件内容
self::$iniConf = config('mysql', static::CONFIG_INDEX);
if (!self::$iniConf) {
throw new \Exception('mysql config not exist');
}
// 获取库分片数量
self::getDbShardingCount();
$type = static::WRITE;
for ($i=0; $i < self::$dbShardingCount; $i++) {
self::$dbConnect[$type.':'.$i] = self::getConnection($type, $i);
}
return self::$dbConnect;
}
/**
* 获取所有分片连接
*
* @return array
*/
public static function getReadConnectionList()
{
// 读取配置文件内容
self::$iniConf = config('mysql', static::CONFIG_INDEX);
if (!self::$iniConf) {
throw new \Exception('mysql config not exist');
}
// 获取库分片数量
self::getDbShardingCount();
$type = static::READ;
for ($i=0; $i < self::$dbShardingCount; $i++) {
self::$dbConnect[$type.':'.$i] = self::getConnection($type, $i);
}
return self::$dbConnect;
}
}
## Mysql工具库
### 主从模式请使用 MysqlBase,分库分表模式请使用 MysqlClusterBase
### MysqlBase 主从模式示例
``` php
<?php
namespace App\Models\demo\mysql;
use Api\PhpUtils\Mysql\MysqlBase;
class TestMysql extends MysqlBase
{
const TABLE_NAME = 'demo'; //表名,必填
const CONFIG_INDEX = 'test'; //yaconf中的index索引,必填
public static function getBook1()
{
return self::get('*',[
'id' => 1
]);
}
public static function insertBook()
{
return self::insert([
'name' => 'bbb',
]);
}
public static function insertManyBook()
{
return self::insert([
['name' => 'bbb'],
['name' => 'ccc'],
]);
}
public static function updateBook()
{
return self::update([
'name' => 'abc',
], ['id'=> 1]);
}
public static function countBook()
{
return self::count();
}
public static function debugBook()
{
return self::debug();
}
public static function logBook()
{
return self::log();
}
public static function lastBook()
{
return self::last();
}
public static function errorBook()
{
return self::error();
}
public static function actionBook()
{
return self::debug();
}
public static function transBook()
{
$t = self::beginTransaction();
var_dump($t);
$t1 = self::insert([
'name' => 'ccc',
]);
$t2 = self::update([
// 'name' => 'abc',
'name' => 'abcdefg',
], ['id'=> 1]);
var_dump([$t1, $t2]);
if($t1 && $t2) {
return self::commit();
}else {
return self::rollback();
}
}
}
```
## MysqlClusterBase 主从分片模式
``` php
<?php
namespace App\Models\demo\mysql;
use Api\PhpUtils\Mysql\MysqlClusterBase;
class TestClusterMysql extends MysqlClusterBase
{
const TABLE_NAME = 'demo_cluster'; //表名,必填
const CONFIG_INDEX = 'orderIndex'; //yaconf中的index索引,必填
const SHARDING_DATABASE_COLUMN = 'order_id'; // 数据库分片字段
const SHARDING_DATABASE_ALGORITHM = 'mod(-7,-5)'; // 数据库分片算法策略
const SHARDING_DATABASE_COMPAT_COLUMN = 'user_id'; // 兼容数据库分片字段
const SHARDING_DATABASE_COMPAT_ALGORITHM = 'mod(-2)'; // 兼容数据库分片算法表达式
public static function getBook()
{
return self::getMaster('*',[
'order_id' => 2105111104548610002,
'user_id' => 0,
// 'id' => 1234567890
]);
}
public static function selectBook()
{
return self::selectMaster('*',[
'order_id' => 2105111104548610002,
'user_id' => 12386,
// 'id' => 1234567890
]);
}
public static function getBook1()
{
return self::get('*',[
'id' => 2
]);
}
public static function insertBook()
{
return self::insert([
'order_id' => 2105111104548610002,
'user_id' => 12386,
'name' => 'bbb',
]);
}
public static function insertManyBook()
{
return self::insert([
[
'order_id' => 2105111104548610002,
'user_id' => 12386,
'name' => 'aaa',
],
[
'order_id' => 2105111104548610002,
'user_id' => 12386,
'name' => 'bbb',
],
[
'order_id' => 2105111104548610002,
'user_id' => 12386,
'name' => 'ccc',
]
]);
}
public static function updateBook()
{
return self::update([
'name' => 'eee',
],
[
'order_id'=> 2105111104548610002,
'user_id'=> 12386,
]);
}
public static function countBook()
{
return self::count([
'order_id' => 2105111104548610002,
'user_id' => 12386,
// 'id' => 1234567890
]);
}
public static function debugBook()
{
return self::debug();
}
public static function logBook()
{
return self::log();
}
public static function lastBook()
{
return self::last();
}
public static function actionBook()
{
return self::debug();
}
public static function errorBook()
{
return self::error();
}
public static function transBook()
{
$t = self::beginTransaction(2105111104548610002);
var_dump($t);
$t1 = self::insert([
'name' => 'aaa',
'order_id' => 2105111104548610002
]);
$t2 = self::update([
// 'name' => 'abc',
'name' => 'abcdefg',
], ['order_id'=> 2105111104548610002]);
var_dump([$t1, $t2]);
if($t1 && $t2) {
echo 'commit';
return self::commit();
}else {
echo 'rollback';
return self::rollback();
}
}
}
```
<?php
namespace Api\PhpUtils\RabbitMq;
use Api\PhpUtils\Log\FileLog;
/**
* RabiitMq工具类
*
* amqp.so verion 1.10.2
*/
class DelayRabbitMq
{
private $conn;
private $channel;
private $exchange;
private $queue;
private $config;
public $work;
public $configIndex;
public function connection()
{
try {
$this->conn = new \AMQPConnection(
$this->config
);
$this->conn->connect();
if ($this->conn->isConnected()) {
FileLog::info('RabbitMqUtil __construct-info:' . '连接成功');
} else {
FileLog::error('RabbitMqUtil __construct-error:' . '连接失败-' . json_encode($this->config));
}
} catch (\Exception $e) {
FileLog::error('RabbitMqUtil __construct-Exception:' . $e->getMessage());
}
}
private function getConfig($configIndex, $flags)
{
$arr = \Yaconf::get('rabbitmq');
$this->config = $arr['common'][$configIndex]['proxy'];
if ($flags != 'produce') {
$this->config = $arr['common'][$configIndex]['consume'];
}
}
/**
* 初始化
* @param string $exchange [交换机]
* @param string $queue [队列名]
* @param string $routing_key [路由]
* @return null
*/
private function init($exchange, $queue, $routingKey, $flags = 'produce')
{
$this->getConfig($this->configIndex, $flags);
$this->connection($flags);
try {
$this->channel = new \AMQPChannel($this->conn);
/*
name: $exchange
type: x-delayed-message
passive: false
durable: true // the exchange will survive server restarts
auto_delete: false //the exchange won't be deleted once the channel is closed.
*/
$this->exchange = new \AMQPExchange($this->channel);
$this->exchange->setName($exchange);
$this->exchange->setType('x-delayed-message');
$this->exchange->setArgument('x-delayed-type', 'direct');
if ($flags == 'produce') {
$this->exchange->setFlags(AMQP_DURABLE);
} else {
$this->exchange->setFlags(AMQP_PASSIVE); //声明一个已存在的交换器的,如果不存在将抛出异常,这个一般用在consume端
}
$this->exchange->declareExchange();
/*
name: $queue
passive: false // don't check if an exchange with the same name exists
durable: true // the queue will survive server restarts
exclusive: false // the queue can be accessed in other channels
auto_delete: false //the queue won't be deleted once the channel is closed.
*/
$this->queue = new \AMQPQueue($this->channel);
$this->queue->setName($queue);
$this->queue->setFlags(AMQP_DURABLE);
$this->queue->declareQueue();
$this->queue->bind($exchange, $routingKey);
} catch (\Exception $e) {
FileLog::error('RabbitMqUtil __init-Exception' . $e->getMessage());
}
}
/**
* 发送消息
* @param string $queue [队列名]
* @param string $sendMessage [发送消息]
* @param int $minute [延迟时间 - 分]
* @param string $exchange [交换机]
* @return null
*/
public function produce($queue, $sendMessage, $minute, $exchange = 'x-delayed-message')
{
$routing_key = $queue;
try {
$this->init($exchange, $queue, $routing_key);
$this->exchange->publish(
$sendMessage,
$routing_key,
AMQP_MANDATORY, //mandatory标志告诉服务器至少将该消息route到一个队列中,否则将消息返还给生产者
array(
'headers' => [
'x-delay' => 1000 * 60 * $minute
]
)
);
$this->conn->disconnect();
} catch (\Exception $e) {
FileLog::error('RabbitMqUtil __produce-Exception:' . $e->getMessage());
}
}
public function process($queue, $exchange = 'x-delayed-message')
{
$routing_key = $queue;
$this->init($exchange, $queue, $routing_key, 'process');
try {
$this->queue->consume([$this, 'processMessage']);
} catch (\Exception $e) {
FileLog::error('RabbitMqUtil __process-Exception:' . $e->getMessage());
}
$this->conn->disconnect();
}
public function processMessage($envelope)
{
$message_body = $envelope->getBody();
$userLogic = $this->work;
if (!is_callable($userLogic)) {
throw new \Exception("需要实现自己的方法逻辑");
}
$is_ack = $userLogic($message_body);
if ($is_ack) {
$this->queue->ack($envelope->getDeliveryTag());
} else {
//$this->queue->nack($envelope->getDeliveryTag());
//无应答 会存放在unacked状态下重连会放回ready,如果发送unack 状态 后会直接放回 ready
}
}
}
#RabbitMq 延迟队列
###新版本使用示例
```php
$mq = new DelayRabbitMq();
$queue = 'delay_list';
$minute = 2; //延是1分钟处理
$message = json_encode(['test' => 'rabbitmq produce', 'reason' => 'send message']);
$mq->produce($queue, $message, $minute);
$mq->work = function ($message) {
$message = json_decode($message,true);
var_dump($message);
return true; //应答
};
$mq->process($queue);
```
\ No newline at end of file
Redis工具库
\ No newline at end of file
<?php
namespace Api\PhpUtils\Redis;
use Api\PhpUtils\Http\Request;
use Api\PhpUtils\Cache\ApcuUtil;
use Api\PhpUtils\Log\FileLog;
use Redis;
class RedisUtil
{
const PREFIX_CODIS_PROXY = '_cp:';
const PREFIX_CODIS_PROXY_LOCK = '_cpl:';
const PREFIX_SENTINEL_HOSTS = '_sh:';
const PREFIX_SENTINEL_HOSTS_LOCK = '_shl:';
const EXPIRE_AFTER_ONE_SECOND = 1;
const EXPIRE_AFTER_FIVE_SECONDS = 5;
const EXPIRE_AFTER_TEN_SECONDS = 10;
const EXPIRE_AFTER_ONE_MINUTE = 60;
const EXPIRE_AFTER_FIVE_MINUTES = 300;
const EXPIRE_AFTER_TEN_MINUTES = 600;
const EXPIRE_AFTER_HALF_HOUR = 1800;
const EXPIRE_AFTER_ONE_HOUR = 3600;
const EXPIRE_AFTER_SIX_HOURS = 21600;
const EXPIRE_AFTER_TWELVE_HOURS = 43200;
const EXPIRE_AFTER_ONE_DAY = 86400;
const EXPIRE_AFTER_THREE_DAYS = 259200;
const EXPIRE_AFTER_ONE_WEEK = 604800;
const EXPIRE_AFTER_FIFTEEN_DAYS = 1296000;
const EXPIRE_AFTER_ONE_MONTH = 2592000;
private static $instances = [];
private $serverName = '';
private $serverRegion = '';
private $master = true; // 哨兵模式/主从模式,默认使用主库
private $serverList = []; // 服务列表(proxy,sentinel)
private $connectServer = '';
private $connectStatus = false;
private $redis = null;
private $mode = '';
private $topom = '';
private $address = ''; //sentinel and redis host:port
private $fallback = '';
private $password = '';
private $timeout = 0.5; //float, value in seconds (optional, default is 0 meaning unlimited)
private $readTimeout = 1.5; // float, value in seconds (optional, default is 0 meaning unlimited)
private $retryInterval = 100; // int, value in milliseconds (optional)
private $serializer = '';
private $prefix = '';
private $select = 0;
private $config = [];
public function getConnectServer()
{
return $this->connectServer;
}
public function getSserverList()
{
return $this->serverList;
}
/**
* 获取实例
*
* @param string $serverName 服务名
* @param array $options 其他参数数组,优先级高于yaconf中的相同配置
* @param string $options['serverRegion'] 服务区域,不同数据中心
* @param bool $options['master'] 哨兵模式/主从模式,默认true使用主库,若使用从库值为false
* @param string $options['serializer'] 序列化选项,包括 none|php|igbinary, msgpack自己实现msgpack_pack|msgpack_unpack, incr等方法只能使用none,set等方法使用none不能存Array或Object
* @param boolean $master
* @return mixed
*/
public static function getInstance($serverName = '', array $options = [])
{
if (empty($serverName)) {
return null;
}
if (empty($options)) {
$instancesKey = $serverName;
} else {
$instancesKey = $serverName . ':' . json_encode($options);
}
if (isset(self::$instances[$instancesKey])) {
return self::$instances[$instancesKey];
}
return self::$instances[$instancesKey] = new Self($serverName, $options);
}
private function __construct($serverName = '', array $options = [])
{
$this->serverName = $serverName;
// 暂时不自动匹配数据中心
// $hostName = explode('.',gethostname());
// if( end($hostName) == 'bj3' ){
// $this->serverRegion = 'wj';
// } else {
// $this->serverRegion = 'sh';
// }
if (isset($options['serverRegion'])) {
$this->serverRegion = $options['serverRegion'];
}
$this->master = $options['master'] ?? true;
$this->serializer = $options['serializer'] ?? '';
$this->initConfigure();
$this->connect();
}
private function initConfigure()
{
$this->config = config('redis', $this->serverName . 'Connect');
if (!empty($this->config)) {
$this->mode = $this->config['mode'] ?? 'codis';
// codis topom
if ($this->mode == 'codis' && isset($this->config['topom'])) {
if ($this->serverRegion) {
$this->topom = $this->config['topom'][$this->serverRegion] ?? '';
} else {
// 不传入数据中心,如果未配置region则直接使用,如果配置了region则默认为使用第一个
$this->topom = is_string($this->config['topom']) ? $this->config['topom'] : current($this->config['topom']);
}
}
//sentinel
if ($this->mode == 'sentinel' && isset($this->config['address'])) {
if ($this->serverRegion) {
$this->address = $this->config['address'][$this->serverRegion] ?? '';
} else {
// 不传入数据中心,如果未配置region则直接使用,如果配置了region则默认为使用第一个
$this->address = is_string($this->config['address']) ? $this->config['address'] : current($this->config['address']);
}
}
//redis master-slave address or redis single address
if ($this->mode == 'redis' && isset($this->config['address'])) {
$masterOrSlave = $this->master ? 'master' : 'slave';
if ($this->serverRegion) {
$this->address = $this->config['address'][$this->serverRegion][$masterOrSlave] ?? '';
if (empty($this->address) && isset($this->config['address'][$this->serverRegion])) {
// 单机,ini中设置了数据中心,但没有设置master/slave
$this->address = is_string($this->config['address'][$this->serverRegion]) ? $this->config['address'][$this->serverRegion] : '';
}
} else {
// 不传入数据中心,ini中未配置region则直接使用
$this->address = $this->config['address'][$masterOrSlave] ?? '';
// 不传入数据中心,ini中如果配置了region,默认使用第一个
if (empty($this->address)) {
$region = current($this->config['address']);
// 兼容ini中未设置master/slave
$this->address = $region[$masterOrSlave] ?? (is_string($region) ? $region : '');
}
// 单机,ini中未配置了region,也未配置master/slave
if (empty($this->address)) {
$this->address = is_string($this->config['address']) ? $this->config['address'] : '';
}
}
}
if (empty($this->serializer) && isset($this->config['serializer'])) {
$this->serializer = $this->config['serializer'] ? strtolower($this->config['serializer']) : 'none';
}
$this->fallback = $this->config['fallback'] ?? '';
$this->password = $this->config['password'] ?? '';
$this->connectTimeout = $this->config['timeout'] ? intval($this->config['timeout']) : 1;
$this->prefix = $this->config['prefix'] ?? '';
$this->select = $this->config['select'] ?? 0;
} else {
throw new \Exception("redis config is empty");
}
}
private function connect()
{
if (!$this->mode) {
return false;
}
switch($this->mode) {
case 'codis':
return $this->initCodis();
break;
case 'sentinel':
return $this->initSentinel();
break;
case 'redis':
return $this->initRedis();
break;
default:
return false;
break;
}
}
private function initCodis()
{
try {
$this->initCodisConnect();
$this->initOptions();
return $this->connectStatus;
}catch (\Exception $e){
return false;
}
}
private function initCodisConnect($retryTime = 1)
{
$codisProxy = $this->getCodisProxyFromApcu();
if($codisProxy === false) {
return false;
}
$this->serverList = $codisProxy;
$proxyNum = count($this->serverList);
$tempServerList = $this->serverList;
$redis = new \Redis();
// 随机选取一个地址,若连接失败,重新选择地址
for ($i = 0; $i < $proxyNum; $i++) {
$index = array_rand($tempServerList);
$this->connectServer = $tempServerList[$index];
list($host, $port) = explode(':', $this->connectServer);
$connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
if (1 || !$connector) {
$j = 0;
while($j < $retryTime){
$connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
$j++;
if($connector){
$this->redis = $redis;
$this->connectStatus = true;
break 2;
}
}
unset($tempServerList[$index]);
}else {
$this->redis = $redis;
$this->connectStatus = true;
break;
}
}
return true;
}
private function initSentinel()
{
try {
$this->initSentinelConnect();
$this->initOptions();
return $this->connectStatus;
}catch (\Exception $e){
return false;
}
}
private function initSentinelConnect($retryTime = 3)
{
$list = $this->getSentinelInfoFromApcu();
if (!$list) {
return false;
}
$server = [];
if($this->master) {
$server = $list['master'];
} else {
if(count($list['slave']) > 1) {
$rand = array_rand($list['slave']);
$server = $list['slave'][$rand];
} else {
$server = current($list['slave']);
}
}
$this->connectServer = $server[0] .':'. $server[1];
$redis = new \Redis();
$host = $server[0];
$port = $server[1];
$connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
if (!$connector) {
$i = 0;
while($i < $retryTime){
$connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
$i++;
if($connector){
$this->redis = $redis;
$this->connectStatus = true;
break;
}
}
}else {
$this->redis = $redis;
$this->connectStatus = true;
}
return true;
}
/**
* 初始化 redis
* @return \Redis
*/
public function initRedis()
{
try {
$this->initRedisConnect();
$this->initOptions();
return $this->connectStatus;
}catch (\Exception $e){
return false;
}
}
private function initRedisConnect($retryTime = 1)
{
if (!$this->address || !strstr($this->address, ":")) {
return null;
}
if ($this->master) {
$this->connectServer = $this->address;
$this->serverList = [$this->address];
} else {
$this->serverList = $address = explode(',', $this->address);
if(count($address) > 1) {
$rand = array_rand($address);
$this->connectServer = $address[$rand];
} else {
$this->connectServer = current($address);
}
}
$redis = new \Redis();
list($host, $port) = explode(':', $this->connectServer);
$connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
if (!$connector) {
$i = 0;
while($i < $retryTime){
$connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
$i++;
if($connector){
$this->redis = $redis;
$this->connectStatus = true;
break;
}
}
}else {
$this->redis = $redis;
$this->connectStatus = true;
}
return true;
}
private function initOptions()
{
if (! $this->connectStatus) {
return false;
}
if ($this->password && !$this->redis->auth($this->password)) {
$this->redis = null;
$this->connectStatus = false;
return false;
}
switch ($this->serializer) {
case 'none':
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); // Don't serialize data
break;
case 'php':
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); // Use built-in serialize/unserialize
break;
case 'igbinary':
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY); // Use igBinary serialize/unserialize
break;
case 'msgpack':
// Undefined class constant 'SERIALIZER_MSGPACK'
// $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_MSGPACK); // Use msgpack serialize/unserialize
// break;
default:
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); // Don't serialize data
break;
}
$this->redis->setOption(Redis::OPT_READ_TIMEOUT, $this->readTimeout);
if ($this->prefix) {
$this->redis->setOption(Redis::OPT_PREFIX, $this->prefix); // use custom prefix on all keys
}
$this->redis->select($this->select); // use custom prefix on all keys
return true;
}
/**
* 从 topom 获取 proxy 列表
*/
private function getTopomServer()
{
if (! $this->topom) {
return false;
}
$url = $this->topom;
$request = new Request();
$res = $request->get($url);
if(isset($res['response'])) {
$res = $res['response'];
}
$serverList = [];
if (!empty($res) && isset($res['stats']['proxy']['models']) && !empty($res['stats']['proxy']['models'])) {
$servers = $res['stats']['proxy']['models'];
foreach ($servers as $s) {
$token = $s['token'];
if (isset($res['stats']['proxy']['stats'][$token]['stats']['online']) && true === $res['stats']['proxy']['stats'][$token]['stats']['online']) {
$serverList[] = $s['proxy_addr'];
}
}
}
if (empty($serverList) && $this->fallback) {
$serverList = explode(',', $this->fallback);
FileLog::waring("topom-server-fallback", " get codis proxy server empty");
}
if ($serverList) {
return $serverList;
}
return false;
}
/**
* 使用SENTINEL slaves命令获取哨兵集群主从节点信息,返回的slave可能含有多个节点
*
* @return boolean|object 成功返回主从信息,失败返回false
*/
private function getMasterSlaveList()
{
$redis = $this->getSentinelInstance();
if(!$redis) {
return false;
}
// $list = $redis->rawCommand('SENTINEL', 'slaves', "wemdia_redis"); // 自媒体哨兵集群名,测试使用
$list = $redis->rawCommand('SENTINEL', 'slaves', $this->serverName);
if( !is_array($list) || !isset($list[0]) ){
return false;
}
foreach ($list as $key => $item)
{
$item = $this->formatSentinelReturn($item);
if( !isset($item['ip']) || !isset($item['port']) || !isset($item['flags']) )
{
FileLog::error("sentinel return error ", json_encode($item));
continue;
}
if( !in_array($item['flags'], array('slave','master')) )
{
FileLog::error("sentinel flags error ", json_encode($item));
continue;
}
if( $item['port'] <=0 )
{
FileLog::error("sentinel port error ", json_encode($item));
continue;
}
$ret['slave'][] = array($item['ip'], $item['port']);
$ret['master'] = array($item['master-host'], $item['master-port']);
}
if( empty($ret['master']) )
{
return false;
}
if( empty($ret['slave']) )
{
$ret['slave'][] = $ret['master'];
}
$ret['slave'] = array_values($ret['slave']);
return $ret;
}
/**
* 获取哨兵连接
*
* @return boolean|object
*/
private function getSentinelInstance()
{
if (!$this->address) {
return false;
}
$this->serverList = $address = explode(',', $this->address);
$addrNum = count($address);
$redis = new \Redis();
// 随机选取一个地址,若连接失败,重新选择地址
for ($i = 0; $i < $addrNum; $i++) {
$index = array_rand($address);
list($host, $port) = explode(':', $address[$index]);
$connector = $redis->connect($host, $port, $this->timeout, null, $this->retryInterval);
if ($connector) {
$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
return $redis;
}
unset($address[$index]);
}
return false;
}
/**
* 格式化哨兵返回信息
*
* @param array $item
* @return array
*/
private function formatSentinelReturn($item)
{
$ret = array();
$count = count($item);
for ($i=0; $i <$count; ){
$ret[$item[$i]] = $item[$i+1];
$i += 2;
}
return $ret;
}
/**
* 从apcu获取codis proxy
*
* @return mixed
*/
private function getCodisProxyFromApcu() {
$res = ApcuUtil::apcu_fetch(self::PREFIX_CODIS_PROXY, $this->serverName);
if ($res !== false) {
// get
if (! isset($res['data']) || ! isset($res['expire_at'])) {
FileLog::waring('get bad value from apcu', $this->serverName . ' ' . self::PREFIX_CODIS_PROXY);
return false;
}
if ($_SERVER['REQUEST_TIME'] >= $res['expire_at']) {
// update 抢占式更新
$this->setCodisProxyToApcu();
}
return $res['data'];
} else {
// set
$data = $this->setCodisProxyToApcu();
if ($data !== false) {
return $data;
} else {
// 自旋 从apcu获取数据,休眠10~50毫秒 usleep(mt_rand(10000,50000))
for ($i = 1; $i < 100; $i++) {
usleep(mt_rand(10000, 50000));
$result = ApcuUtil::apcu_fetch(self::PREFIX_CODIS_PROXY, $this->serverName);
if ($result !== false) {
return $result['data'] ?? false;
}
if ($i % 10 === 0) {
$data = $this->setCodisProxyToApcu();
if ($data !== false) {
return $data;
}
}
}
return false;
}
}
}
/**
* 将codis proxy设置到apcu中,缓存10秒钟
*
* @return mixed
*/
private function setCodisProxyToApcu() {
if (ApcuUtil::apcu_add_one(self::PREFIX_CODIS_PROXY_LOCK, $this->serverName, 1, 2)) {
// 抢到锁,处理业务
$data = $this->getTopomServer();
if ($data !== false) {
$value = array(
'data' => $data,
'expire_at' => $_SERVER['REQUEST_TIME'] + 6
);
ApcuUtil::retry_apcu_store_one(self::PREFIX_CODIS_PROXY, $this->serverName, $value, 10);
ApcuUtil::apcu_delete(self::PREFIX_CODIS_PROXY_LOCK, $this->serverName);
return $data;
}
}
return false;
}
/**
* 从apcu 获取 sentinel master-slave list
*
* @return mixed
*/
private function getSentinelInfoFromApcu() {
$res = ApcuUtil::apcu_fetch(self::PREFIX_SENTINEL_HOSTS, $this->serverName);
if ($res !== false) {
// get
if (! isset($res['data']) || ! isset($res['expire_at'])) {
FileLog::waring('get bad value from apcu', $this->serverName . ' ' . self::PREFIX_SENTINEL_HOSTS);
return false;
}
if ($_SERVER['REQUEST_TIME'] >= $res['expire_at']) {
// update 抢占式更新
$this->setSentinelInfoToApcu();
}
return $res['data'];
} else {
// set
$data = $this->setSentinelInfoToApcu();
if ($data !== false) {
return $data;
} else {
// 自旋 从apcu获取数据,休眠10~50毫秒 usleep(mt_rand(10000,50000))
for ($i = 1; $i < 100; $i++) {
usleep(mt_rand(10000, 50000));
$result = ApcuUtil::apcu_fetch(self::PREFIX_SENTINEL_HOSTS, $this->serverName);
if ($result !== false) {
return $result['data'] ?? false;
}
if ($i % 10 === 0) {
$data = $this->setSentinelInfoToApcu();
if ($data !== false) {
return $data;
}
}
}
return false;
}
}
}
/**
* 将sentinel master-slave list 设置到apcu中,缓存10秒钟
*
* @return mixed
*/
private function setSentinelInfoToApcu() {
if (ApcuUtil::apcu_add_one(self::PREFIX_SENTINEL_HOSTS_LOCK, $this->serverName, 1, 2)) {
// 抢到锁,处理业务
$data = $this->getMasterSlaveList();
if ($data !== false) {
$value = array(
'data' => $data,
'expire_at' => $_SERVER['REQUEST_TIME'] + 6
);
ApcuUtil::retry_apcu_store_one(self::PREFIX_SENTINEL_HOSTS, $this->serverName, $value, 10);
ApcuUtil::apcu_delete(self::PREFIX_SENTINEL_HOSTS_LOCK, $this->serverName);
return $data;
}
}
return false;
}
public function __call($method, array $args)
{
if (!$this->redis) {
return false;
}
return call_user_func_array([$this->redis, $method], $args);
}
}
Validate工具库
\ No newline at end of file
<?php
namespace Api\PhpUtils\Validate;
use Closure;
class Validate
{
/**
* 自定义验证类型
* @var array
*/
protected $type = [];
/**
* 验证类型别名
* @var array
*/
protected $alias = [
'>' => 'gt', '>=' => 'egt', '<' => 'lt', '<=' => 'elt', '=' => 'eq', 'same' => 'eq',
];
/**
* 当前验证规则
* @var array
*/
protected $rule = [];
/**
* 验证提示信息
* @var array
*/
protected $message = [];
/**
* 验证字段描述
* @var array
*/
protected $field = [];
/**
* 默认规则提示
* @var array
*/
protected $typeMsg = [
'require' => ':attribute require',
'must' => ':attribute must',
'number' => ':attribute must be numeric',
'integer' => ':attribute must be integer',
'float' => ':attribute must be float',
'boolean' => ':attribute must be bool',
'email' => ':attribute not a valid email address',
'mobile' => ':attribute not a valid mobile',
'array' => ':attribute must be a array',
'accepted' => ':attribute must be yes,on or 1',
'date' => ':attribute not a valid datetime',
'image' => ':attribute not a valid image',
'alpha' => ':attribute must be alpha',
'alphaNum' => ':attribute must be alpha-numeric',
'alphaDash' => ':attribute must be alpha-numeric, dash, underscore',
'activeUrl' => ':attribute not a valid domain or ip',
'chs' => ':attribute must be chinese',
'chsAlpha' => ':attribute must be chinese or alpha',
'chsAlphaNum' => ':attribute must be chinese,alpha-numeric',
'chsDash' => ':attribute must be chinese,alpha-numeric,underscore, dash',
'url' => ':attribute not a valid url',
'ip' => ':attribute not a valid ip',
'dateFormat' => ':attribute must be dateFormat of :rule',
'in' => ':attribute must be in :rule',
'notIn' => ':attribute be notin :rule',
'between' => ':attribute must between :1 - :2',
'notBetween' => ':attribute not between :1 - :2',
'length' => 'size of :attribute must be :rule',
'max' => 'max size of :attribute must be :rule',
'min' => 'min size of :attribute must be :rule',
'after' => ':attribute cannot be less than :rule',
'before' => ':attribute cannot exceed :rule',
'expire' => ':attribute not within :rule',
'allowIp' => 'access IP is not allowed',
'denyIp' => 'access IP denied',
'confirm' => ':attribute out of accord with :2',
'different' => ':attribute cannot be same with :2',
'egt' => ':attribute must greater than or equal :rule',
'gt' => ':attribute must greater than :rule',
'elt' => ':attribute must less than or equal :rule',
'lt' => ':attribute must less than :rule',
'eq' => ':attribute must equal :rule',
'unique' => ':attribute has exists',
'regex' => ':attribute not conform to the rules',
'method' => 'invalid Request method'
];
/**
* 当前验证场景
* @var string
*/
protected $currentScene;
/**
* 内置正则验证规则
* @var array
*/
protected $defaultRegex = [
'alpha' => '/^[A-Za-z]+$/',
'alphaNum' => '/^[A-Za-z0-9]+$/',
'alphaDash' => '/^[A-Za-z0-9\-\_]+$/',
'chs' => '/^[\x{4e00}-\x{9fa5}\x{9fa6}-\x{9fef}\x{3400}-\x{4db5}\x{20000}-\x{2ebe0}]+$/u',
'chsAlpha' => '/^[\x{4e00}-\x{9fa5}\x{9fa6}-\x{9fef}\x{3400}-\x{4db5}\x{20000}-\x{2ebe0}a-zA-Z]+$/u',
'chsAlphaNum' => '/^[\x{4e00}-\x{9fa5}\x{9fa6}-\x{9fef}\x{3400}-\x{4db5}\x{20000}-\x{2ebe0}a-zA-Z0-9]+$/u',
'chsDash' => '/^[\x{4e00}-\x{9fa5}\x{9fa6}-\x{9fef}\x{3400}-\x{4db5}\x{20000}-\x{2ebe0}a-zA-Z0-9\_\-]+$/u',
'mobile' => '/^1[3-9]\d{9}$/',
'idCard' => '/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}$)/',
'zip' => '/\d{6}/',
];
/**
* Filter_var 规则
* @var array
*/
protected $filter = [
'email' => FILTER_VALIDATE_EMAIL,
'ip' => [FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6],
'integer' => FILTER_VALIDATE_INT,
'url' => FILTER_VALIDATE_URL,
'macAddr' => FILTER_VALIDATE_MAC,
'float' => FILTER_VALIDATE_FLOAT,
];
/**
* 验证场景定义
* @var array
*/
protected $scene = [];
/**
* 验证失败错误信息
* @var string|array
*/
protected $error = [];
/**
* 是否批量验证
* @var bool
*/
protected $batch = false;
/**
* 验证失败是否抛出异常
* @var bool
*/
protected $failException = false;
/**
* 场景需要验证的规则
* @var array
*/
protected $only = [];
/**
* 场景需要移除的验证规则
* @var array
*/
protected $remove = [];
/**
* 场景需要追加的验证规则
* @var array
*/
protected $append = [];
/**
* 验证正则定义
* @var array
*/
protected $regex = [];
/**
* 添加字段验证规则
* @access protected
* @param string|array $name 字段名称或者规则数组
* @param mixed $rule 验证规则或者字段描述信息
* @return Validate
*/
public function rule($name, $rule = '')
{
if (is_array($name)) {
$this->rule = $name + $this->rule;
if (is_array($rule)) {
$this->field = array_merge($this->field, $rule);
}
} else {
$this->rule[$name] = $rule;
}
return $this;
}
/**
* 注册验证(类型)规则
* @access public
* @param string $type 验证规则类型
* @param callable $callback callback方法(或闭包)
* @param string $message 验证失败提示信息
* @return $this
*/
public function extend(string $type, callable $callback = null, string $message = null)
{
$this->type[$type] = $callback;
if ($message) {
$this->typeMsg[$type] = $message;
}
return $this;
}
/**
* 设置验证规则的默认提示信息
* @access public
* @param string|array $type 验证规则类型名称或者数组
* @param string $msg 验证提示信息
* @return void
*/
public function setTypeMsg($type, string $msg = null): void
{
if (is_array($type)) {
$this->typeMsg = array_merge($this->typeMsg, $type);
} else {
$this->typeMsg[$type] = $msg;
}
}
/**
* 设置提示信息
* @access public
* @param array $message 错误信息
* @return Validate
*/
public function message(array $message)
{
$this->message = array_merge($this->message, $message);
return $this;
}
/**
* 设置验证场景
* @access public
* @param string $name 场景名
* @return $this
*/
public function scene(string $name)
{
// 设置当前场景
$this->currentScene = $name;
return $this;
}
/**
* 判断是否存在某个验证场景
* @access public
* @param string $name 场景名
* @return bool
*/
public function hasScene(string $name): bool
{
return isset($this->scene[$name]) || method_exists($this, 'scene' . $name);
}
/**
* 设置批量验证
* @access public
* @param bool $batch 是否批量验证
* @return $this
*/
public function batch(bool $batch = true)
{
$this->batch = $batch;
return $this;
}
/**
* 设置验证失败后是否抛出异常
* @access protected
* @param bool $fail 是否抛出异常
* @return $this
*/
public function failException(bool $fail = true)
{
$this->failException = $fail;
return $this;
}
/**
* 指定需要验证的字段列表
* @access public
* @param array $fields 字段名
* @return $this
*/
public function only(array $fields)
{
$this->only = $fields;
return $this;
}
/**
* 移除某个字段的验证规则
* @access public
* @param string|array $field 字段名
* @param mixed $rule 验证规则 true 移除所有规则
* @return $this
*/
public function remove($field, $rule = null)
{
if (is_array($field)) {
foreach ($field as $key => $rule) {
if (is_int($key)) {
$this->remove($rule);
} else {
$this->remove($key, $rule);
}
}
} else {
if (is_string($rule)) {
$rule = explode('|', $rule);
}
$this->remove[$field] = $rule;
}
return $this;
}
/**
* 追加某个字段的验证规则
* @access public
* @param string|array $field 字段名
* @param mixed $rule 验证规则
* @return $this
*/
public function append($field, $rule = null)
{
if (is_array($field)) {
foreach ($field as $key => $rule) {
$this->append($key, $rule);
}
} else {
if (is_string($rule)) {
$rule = explode('|', $rule);
}
$this->append[$field] = $rule;
}
return $this;
}
/**
* 数据自动验证
* @access public
* @param array $data 数据
* @param array $rules 验证规则
* @return bool
*/
public function check(array $data, array $rules = []): bool
{
$this->error = [];
if ($this->currentScene) {
$this->getScene($this->currentScene);
}
if (empty($rules)) {
// 读取验证规则
$rules = $this->rule;
}
foreach ($this->append as $key => $rule) {
if (!isset($rules[$key])) {
$rules[$key] = $rule;
unset($this->append[$key]);
}
}
foreach ($rules as $key => $rule) {
if (strpos($key, '|')) {
// 字段|描述 用于指定属性名称
[$key, $title] = explode('|', $key);
} else {
$title = $this->field[$key] ?? $key;
}
// 场景检测
if (!empty($this->only) && !in_array($key, $this->only)) {
continue;
}
// 获取数据 支持二维数组
$value = $this->getDataValue($data, $key);
// 字段验证
if ($rule instanceof Closure) {
$result = call_user_func_array($rule, [$value, $data]);
} elseif ($rule instanceof ValidateRule) {
// 验证因子
$result = $this->checkItem($key, $value, $rule->getRule(), $data, $rule->getTitle() ?: $title, $rule->getMsg());
} else {
$result = $this->checkItem($key, $value, $rule, $data, $title);
}
if (true !== $result) {
// 没有返回true 则表示验证失败
if (!empty($this->batch)) {
// 批量验证
$this->error[$key] = $result;
} else {
$this->error = $result;
return false;
}
}
}
return !empty($this->error) ? false : true;
}
/**
* 根据验证规则验证数据
* @access public
* @param mixed $value 字段值
* @param mixed $rules 验证规则
* @return bool
*/
public function checkRule($value, $rules): bool
{
if ($rules instanceof Closure) {
return call_user_func_array($rules, [$value]);
} elseif ($rules instanceof ValidateRule) {
$rules = $rules->getRule();
} elseif (is_string($rules)) {
$rules = explode('|', $rules);
}
foreach ($rules as $key => $rule) {
if ($rule instanceof Closure) {
$result = call_user_func_array($rule, [$value]);
} else {
// 判断验证类型
[$type, $rule] = $this->getValidateType($key, $rule);
$callback = $this->type[$type] ?? [$this, $type];
$result = call_user_func_array($callback, [$value, $rule]);
}
if (true !== $result) {
return $result;
}
}
return true;
}
/**
* 验证单个字段规则
* @access protected
* @param string $field 字段名
* @param mixed $value 字段值
* @param mixed $rules 验证规则
* @param array $data 数据
* @param string $title 字段描述
* @param array $msg 提示信息
* @return mixed
*/
protected function checkItem(string $field, $value, $rules, $data, string $title = '', array $msg = [])
{
if (isset($this->remove[$field]) && true === $this->remove[$field] && empty($this->append[$field])) {
// 字段已经移除 无需验证
return true;
}
// 支持多规则验证 require|in:a,b,c|... 或者 ['require','in'=>'a,b,c',...]
if (is_string($rules)) {
$rules = explode('|', $rules);
}
if (isset($this->append[$field])) {
// 追加额外的验证规则
$rules = array_unique(array_merge($rules, $this->append[$field]), SORT_REGULAR);
unset($this->append[$field]);
}
if (empty($rules)) {
return true;
}
$i = 0;
foreach ($rules as $key => $rule) {
if ($rule instanceof \Closure) {
$result = call_user_func_array($rule, [$value, $data]);
$info = is_numeric($key) ? '' : $key;
} else {
// 判断验证类型
[$type, $rule, $info] = $this->getValidateType($key, $rule);
if (isset($this->append[$field]) && in_array($info, $this->append[$field])) {
} elseif (isset($this->remove[$field]) && in_array($info, $this->remove[$field])) {
// 规则已经移除
$i++;
continue;
}
if (isset($this->type[$type])) {
$result = call_user_func_array($this->type[$type], [$value, $rule, $data, $field, $title]);
} elseif ('must' == $info || 0 === strpos($info, 'require') || (!is_null($value) && '' !== $value)) {
$result = call_user_func_array([$this, $type], [$value, $rule, $data, $field, $title]);
} else {
$result = true;
}
}
if (false === $result) {
// 验证失败 返回错误信息
if (!empty($msg[$i])) {
$message = $msg[$i];
if (is_string($message) && strpos($message, '{%') === 0) {
$message = mb_substr($message, 2, -1);
}
} else {
$message = $this->getRuleMsg($field, $title, $info, $rule);
}
return $message;
} elseif (true !== $result) {
// 返回自定义错误信息
if (is_string($result) && false !== strpos($result, ':')) {
$result = str_replace(':attribute', $title, $result);
if (strpos($result, ':rule') && is_scalar($rule)) {
$result = str_replace(':rule', (string) $rule, $result);
}
}
return $result;
}
$i++;
}
return $result ?? true;
}
/**
* 获取当前验证类型及规则
* @access public
* @param mixed $key
* @param mixed $rule
* @return array
*/
protected function getValidateType($key, $rule): array
{
// 判断验证类型
if (!is_numeric($key)) {
if (isset($this->alias[$key])) {
// 判断别名
$key = $this->alias[$key];
}
return [$key, $rule, $key];
}
if (strpos($rule, ':')) {
[$type, $rule] = explode(':', $rule, 2);
if (isset($this->alias[$type])) {
// 判断别名
$type = $this->alias[$type];
}
$info = $type;
} elseif (method_exists($this, $rule)) {
$type = $rule;
$info = $rule;
$rule = '';
} else {
$type = 'is';
$info = $rule;
}
return [$type, $rule, $info];
}
/**
* 验证是否和某个字段的值一致
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @param string $field 字段名
* @return bool
*/
public function confirm($value, $rule, array $data = [], string $field = ''): bool
{
if ('' == $rule) {
if (strpos($field, '_confirm')) {
$rule = strstr($field, '_confirm', true);
} else {
$rule = $field . '_confirm';
}
}
return $this->getDataValue($data, $rule) === $value;
}
/**
* 验证是否和某个字段的值是否不同
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function different($value, $rule, array $data = []): bool
{
return $this->getDataValue($data, $rule) != $value;
}
/**
* 验证是否大于等于某个值
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function egt($value, $rule, array $data = []): bool
{
return $value >= $this->getDataValue($data, $rule);
}
/**
* 验证是否大于某个值
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function gt($value, $rule, array $data = []): bool
{
return $value > $this->getDataValue($data, $rule);
}
/**
* 验证是否小于等于某个值
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function elt($value, $rule, array $data = []): bool
{
return $value <= $this->getDataValue($data, $rule);
}
/**
* 验证是否小于某个值
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function lt($value, $rule, array $data = []): bool
{
return $value < $this->getDataValue($data, $rule);
}
/**
* 验证是否等于某个值
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function eq($value, $rule): bool
{
return $value == $rule;
}
/**
* 必须验证
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function must($value, $rule = null): bool
{
return !empty($value) || '0' == $value;
}
/**
* 验证字段值是否为有效格式
* @access public
* @param mixed $value 字段值
* @param string $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function is($value, string $rule, array $data = []): bool
{
switch ($rule) {
case 'require':
// 必须
$result = !empty($value) || '0' == $value;
break;
case 'accepted':
// 接受
$result = in_array($value, ['1', 'on', 'yes']);
break;
case 'date':
// 是否是一个有效日期
$result = false !== strtotime($value);
break;
case 'activeUrl':
// 是否为有效的网址
$result = checkdnsrr($value);
break;
case 'boolean':
case 'bool':
// 是否为布尔值
$result = in_array($value, [true, false, 0, 1, '0', '1'], true);
break;
case 'number':
$result = ctype_digit((string) $value);
break;
case 'alphaNum':
$result = ctype_alnum($value);
break;
case 'array':
// 是否为数组
$result = is_array($value);
break;
case 'token':
$result = $this->token($value, '__token__', $data);
break;
default:
if (isset($this->type[$rule])) {
// 注册的验证规则
$result = call_user_func_array($this->type[$rule], [$value]);
} elseif (function_exists('ctype_' . $rule)) {
// ctype验证规则
$ctypeFun = 'ctype_' . $rule;
$result = $ctypeFun($value);
} elseif (isset($this->filter[$rule])) {
// Filter_var验证规则
$result = $this->filter($value, $this->filter[$rule]);
} else {
// 正则验证
$result = $this->regex($value, $rule);
}
}
return $result;
}
// 判断图像类型
protected function getImageType($image)
{
if (function_exists('exif_imagetype')) {
return exif_imagetype($image);
}
try {
$info = getimagesize($image);
return $info ? $info[2] : false;
} catch (\Exception $e) {
return false;
}
}
/**
* 验证是否为合格的域名或者IP 支持A,MX,NS,SOA,PTR,CNAME,AAAA,A6, SRV,NAPTR,TXT 或者 ANY类型
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function activeUrl(string $value, string $rule = 'MX'): bool
{
if (!in_array($rule, ['A', 'MX', 'NS', 'SOA', 'PTR', 'CNAME', 'AAAA', 'A6', 'SRV', 'NAPTR', 'TXT', 'ANY'])) {
$rule = 'MX';
}
return checkdnsrr($value, $rule);
}
/**
* 验证是否有效IP
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则 ipv4 ipv6
* @return bool
*/
public function ip($value, string $rule = 'ipv4'): bool
{
if (!in_array($rule, ['ipv4', 'ipv6'])) {
$rule = 'ipv4';
}
return $this->filter($value, [FILTER_VALIDATE_IP, 'ipv6' == $rule ? FILTER_FLAG_IPV6 : FILTER_FLAG_IPV4]);
}
/**
* 验证时间和日期是否符合指定格式
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function dateFormat($value, $rule): bool
{
$info = date_parse_from_format($rule, $value);
return 0 == $info['warning_count'] && 0 == $info['error_count'];
}
/**
* 使用filter_var方式验证
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function filter($value, $rule): bool
{
if (is_string($rule) && strpos($rule, ',')) {
[$rule, $param] = explode(',', $rule);
} elseif (is_array($rule)) {
$param = $rule[1] ?? 0;
$rule = $rule[0];
} else {
$param = 0;
}
return false !== filter_var($value, is_int($rule) ? $rule : filter_id($rule), $param);
}
/**
* 验证某个字段等于某个值的时候必须
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function requireIf($value, $rule, array $data = []): bool
{
[$field, $val] = explode(',', $rule);
if ($this->getDataValue($data, $field) == $val) {
return !empty($value) || '0' == $value;
}
return true;
}
/**
* 通过回调方法验证某个字段是否必须
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function requireCallback($value, $rule, array $data = []): bool
{
$result = call_user_func_array([$this, $rule], [$value, $data]);
if ($result) {
return !empty($value) || '0' == $value;
}
return true;
}
/**
* 验证某个字段有值的情况下必须
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function requireWith($value, $rule, array $data = []): bool
{
$val = $this->getDataValue($data, $rule);
if (!empty($val)) {
return !empty($value) || '0' == $value;
}
return true;
}
/**
* 验证某个字段没有值的情况下必须
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function requireWithout($value, $rule, array $data = []): bool
{
$val = $this->getDataValue($data, $rule);
if (empty($val)) {
return !empty($value) || '0' == $value;
}
return true;
}
/**
* 验证是否在范围内
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function in($value, $rule): bool
{
return in_array($value, is_array($rule) ? $rule : explode(',', $rule));
}
/**
* 验证是否不在某个范围
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function notIn($value, $rule): bool
{
return !in_array($value, is_array($rule) ? $rule : explode(',', $rule));
}
/**
* between验证数据
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function between($value, $rule): bool
{
if (is_string($rule)) {
$rule = explode(',', $rule);
}
[$min, $max] = $rule;
return $value >= $min && $value <= $max;
}
/**
* 使用notbetween验证数据
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function notBetween($value, $rule): bool
{
if (is_string($rule)) {
$rule = explode(',', $rule);
}
[$min, $max] = $rule;
return $value < $min || $value > $max;
}
/**
* 验证数据长度
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function length($value, $rule): bool
{
if (is_array($value)) {
$length = count($value);
} else {
$length = mb_strlen((string) $value);
}
if (is_string($rule) && strpos($rule, ',')) {
// 长度区间
[$min, $max] = explode(',', $rule);
return $length >= $min && $length <= $max;
}
// 指定长度
return $length == $rule;
}
/**
* 验证数据最大长度
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function max($value, $rule): bool
{
if (is_array($value)) {
$length = count($value);
} else {
$length = mb_strlen((string) $value);
}
return $length <= $rule;
}
/**
* 验证数据最小长度
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function min($value, $rule): bool
{
if (is_array($value)) {
$length = count($value);
} else {
$length = mb_strlen((string) $value);
}
return $length >= $rule;
}
/**
* 验证日期
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function after($value, $rule, array $data = []): bool
{
return strtotime($value) >= strtotime($rule);
}
/**
* 验证日期
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function before($value, $rule, array $data = []): bool
{
return strtotime($value) <= strtotime($rule);
}
/**
* 验证日期
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function afterWith($value, $rule, array $data = []): bool
{
$rule = $this->getDataValue($data, $rule);
return !is_null($rule) && strtotime($value) >= strtotime($rule);
}
/**
* 验证日期
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @param array $data 数据
* @return bool
*/
public function beforeWith($value, $rule, array $data = []): bool
{
$rule = $this->getDataValue($data, $rule);
return !is_null($rule) && strtotime($value) <= strtotime($rule);
}
/**
* 验证有效期
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function expire($value, $rule): bool
{
if (is_string($rule)) {
$rule = explode(',', $rule);
}
[$start, $end] = $rule;
if (!is_numeric($start)) {
$start = strtotime($start);
}
if (!is_numeric($end)) {
$end = strtotime($end);
}
return time() >= $start && time() <= $end;
}
/**
* 验证IP许可
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function allowIp($value, $rule): bool
{
return in_array($value, is_array($rule) ? $rule : explode(',', $rule));
}
/**
* 验证IP禁用
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则
* @return bool
*/
public function denyIp($value, $rule): bool
{
return !in_array($value, is_array($rule) ? $rule : explode(',', $rule));
}
/**
* 使用正则验证数据
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则 正则规则或者预定义正则名
* @return bool
*/
public function regex($value, $rule): bool
{
if (isset($this->regex[$rule])) {
$rule = $this->regex[$rule];
} elseif (isset($this->defaultRegex[$rule])) {
$rule = $this->defaultRegex[$rule];
}
if (is_string($rule) && 0 !== strpos($rule, '/') && !preg_match('/\/[imsU]{0,4}$/', $rule)) {
// 不是正则表达式则两端补上/
$rule = '/^' . $rule . '$/';
}
return is_scalar($value) && 1 === preg_match($rule, (string) $value);
}
/**
* 获取错误信息
* @return array|string
*/
public function getError()
{
return $this->error;
}
/**
* 获取数据值
* @access protected
* @param array $data 数据
* @param string $key 数据标识 支持二维
* @return mixed
*/
protected function getDataValue(array $data, $key)
{
if (is_numeric($key)) {
$value = $key;
} elseif (is_string($key) && strpos($key, '.')) {
// 支持多维数组验证
foreach (explode('.', $key) as $key) {
if (!isset($data[$key])) {
$value = null;
break;
}
$value = $data = $data[$key];
}
} else {
$value = $data[$key] ?? null;
}
return $value;
}
/**
* 获取验证规则的错误提示信息
* @access protected
* @param string $attribute 字段英文名
* @param string $title 字段描述名
* @param string $type 验证规则名称
* @param mixed $rule 验证规则数据
* @return string|array
*/
protected function getRuleMsg(string $attribute, string $title, string $type, $rule)
{
if (isset($this->message[$attribute . '.' . $type])) {
$msg = $this->message[$attribute . '.' . $type];
} elseif (isset($this->message[$attribute][$type])) {
$msg = $this->message[$attribute][$type];
} elseif (isset($this->message[$attribute])) {
$msg = $this->message[$attribute];
} elseif (isset($this->typeMsg[$type])) {
$msg = $this->typeMsg[$type];
} elseif (0 === strpos($type, 'require')) {
$msg = $this->typeMsg['require'];
} else {
$msg = $title . ' 不符合规范';
}
if (is_array($msg)) {
return $this->errorMsgIsArray($msg, $rule, $title);
}
return $this->parseErrorMsg($msg, $rule, $title);
}
/**
* 获取验证规则的错误提示信息
* @access protected
* @param string $msg 错误信息
* @param mixed $rule 验证规则数据
* @param string $title 字段描述名
* @return array
*/
protected function parseErrorMsg(string $msg, $rule, string $title)
{
if (0 === strpos($msg, '{%')) {
$message = mb_substr($msg, 2, -1);
}
if (is_array($msg)) {
return $this->errorMsgIsArray($msg, $rule, $title);
}
// rule若是数组则转为字符串
if (is_array($rule)) {
$rule = implode(',', $rule);
}
if (is_scalar($rule) && false !== strpos($msg, ':')) {
// 变量替换
if (is_string($rule) && strpos($rule, ',')) {
$array = array_pad(explode(',', $rule), 3, '');
} else {
$array = array_pad([], 3, '');
}
$msg = str_replace(
[':attribute', ':1', ':2', ':3'],
[$title, $array[0], $array[1], $array[2]],
$msg
);
if (strpos($msg, ':rule')) {
$msg = str_replace(':rule', (string) $rule, $msg);
}
}
return $msg;
}
/**
* 错误信息数组处理
* @access protected
* @param array $msg 错误信息
* @param mixed $rule 验证规则数据
* @param string $title 字段描述名
* @return array
*/
protected function errorMsgIsArray(array $msg, $rule, string $title)
{
foreach ($msg as $key => $val) {
if (is_string($val)) {
$msg[$key] = $this->parseErrorMsg($val, $rule, $title);
}
}
return $msg;
}
/**
* 获取数据验证的场景
* @access protected
* @param string $scene 验证场景
* @return void
*/
protected function getScene(string $scene): void
{
$this->only = $this->append = $this->remove = [];
if (method_exists($this, 'scene' . $scene)) {
call_user_func([$this, 'scene' . $scene]);
} elseif (isset($this->scene[$scene])) {
// 如果设置了验证适用场景
$this->only = $this->scene[$scene];
}
}
/**
* 动态方法 直接调用is方法进行验证
* @access public
* @param string $method 方法名
* @param array $args 调用参数
* @return bool
*/
public function __call($method, $args)
{
if ('is' == strtolower(substr($method, 0, 2))) {
$method = substr($method, 2);
}
array_push($args, lcfirst($method));
return call_user_func_array([$this, 'is'], $args);
}
}
\ No newline at end of file
<?php
namespace Api\PhpUtils\Validate;
/**
* Class ValidateRule
* @package think\validate
* @method ValidateRule confirm(mixed $rule, string $msg = '') static 验证是否和某个字段的值一致
* @method ValidateRule different(mixed $rule, string $msg = '') static 验证是否和某个字段的值是否不同
* @method ValidateRule egt(mixed $rule, string $msg = '') static 验证是否大于等于某个值
* @method ValidateRule gt(mixed $rule, string $msg = '') static 验证是否大于某个值
* @method ValidateRule elt(mixed $rule, string $msg = '') static 验证是否小于等于某个值
* @method ValidateRule lt(mixed $rule, string $msg = '') static 验证是否小于某个值
* @method ValidateRule eg(mixed $rule, string $msg = '') static 验证是否等于某个值
* @method ValidateRule in(mixed $rule, string $msg = '') static 验证是否在范围内
* @method ValidateRule notIn(mixed $rule, string $msg = '') static 验证是否不在某个范围
* @method ValidateRule between(mixed $rule, string $msg = '') static 验证是否在某个区间
* @method ValidateRule notBetween(mixed $rule, string $msg = '') static 验证是否不在某个区间
* @method ValidateRule length(mixed $rule, string $msg = '') static 验证数据长度
* @method ValidateRule max(mixed $rule, string $msg = '') static 验证数据最大长度
* @method ValidateRule min(mixed $rule, string $msg = '') static 验证数据最小长度
* @method ValidateRule after(mixed $rule, string $msg = '') static 验证日期
* @method ValidateRule before(mixed $rule, string $msg = '') static 验证日期
* @method ValidateRule expire(mixed $rule, string $msg = '') static 验证有效期
* @method ValidateRule allowIp(mixed $rule, string $msg = '') static 验证IP许可
* @method ValidateRule denyIp(mixed $rule, string $msg = '') static 验证IP禁用
* @method ValidateRule regex(mixed $rule, string $msg = '') static 使用正则验证数据
* @method ValidateRule token(mixed $rule='__token__', string $msg = '') static 验证表单令牌
* @method ValidateRule is(mixed $rule, string $msg = '') static 验证字段值是否为有效格式
* @method ValidateRule isRequire(mixed $rule = null, string $msg = '') static 验证字段必须
* @method ValidateRule isNumber(mixed $rule = null, string $msg = '') static 验证字段值是否为数字
* @method ValidateRule isArray(mixed $rule = null, string $msg = '') static 验证字段值是否为数组
* @method ValidateRule isInteger(mixed $rule = null, string $msg = '') static 验证字段值是否为整形
* @method ValidateRule isFloat(mixed $rule = null, string $msg = '') static 验证字段值是否为浮点数
* @method ValidateRule isMobile(mixed $rule = null, string $msg = '') static 验证字段值是否为手机
* @method ValidateRule isIdCard(mixed $rule = null, string $msg = '') static 验证字段值是否为身份证号码
* @method ValidateRule isChs(mixed $rule = null, string $msg = '') static 验证字段值是否为中文
* @method ValidateRule isChsDash(mixed $rule = null, string $msg = '') static 验证字段值是否为中文字母及下划线
* @method ValidateRule isChsAlpha(mixed $rule = null, string $msg = '') static 验证字段值是否为中文和字母
* @method ValidateRule isChsAlphaNum(mixed $rule = null, string $msg = '') static 验证字段值是否为中文字母和数字
* @method ValidateRule isDate(mixed $rule = null, string $msg = '') static 验证字段值是否为有效格式
* @method ValidateRule isBool(mixed $rule = null, string $msg = '') static 验证字段值是否为布尔值
* @method ValidateRule isAlpha(mixed $rule = null, string $msg = '') static 验证字段值是否为字母
* @method ValidateRule isAlphaDash(mixed $rule = null, string $msg = '') static 验证字段值是否为字母和下划线
* @method ValidateRule isAlphaNum(mixed $rule = null, string $msg = '') static 验证字段值是否为字母和数字
* @method ValidateRule isAccepted(mixed $rule = null, string $msg = '') static 验证字段值是否为yes, on, 或是 1
* @method ValidateRule isEmail(mixed $rule = null, string $msg = '') static 验证字段值是否为有效邮箱格式
* @method ValidateRule isUrl(mixed $rule = null, string $msg = '') static 验证字段值是否为有效URL地址
* @method ValidateRule activeUrl(mixed $rule, string $msg = '') static 验证是否为合格的域名或者IP
* @method ValidateRule ip(mixed $rule, string $msg = '') static 验证是否有效IP
* @method ValidateRule fileExt(mixed $rule, string $msg = '') static 验证文件后缀
* @method ValidateRule fileMime(mixed $rule, string $msg = '') static 验证文件类型
* @method ValidateRule fileSize(mixed $rule, string $msg = '') static 验证文件大小
* @method ValidateRule image(mixed $rule, string $msg = '') static 验证图像文件
* @method ValidateRule method(mixed $rule, string $msg = '') static 验证请求类型
* @method ValidateRule dateFormat(mixed $rule, string $msg = '') static 验证时间和日期是否符合指定格式
* @method ValidateRule unique(mixed $rule, string $msg = '') static 验证是否唯一
* @method ValidateRule behavior(mixed $rule, string $msg = '') static 使用行为类验证
* @method ValidateRule filter(mixed $rule, string $msg = '') static 使用filter_var方式验证
* @method ValidateRule requireIf(mixed $rule, string $msg = '') static 验证某个字段等于某个值的时候必须
* @method ValidateRule requireCallback(mixed $rule, string $msg = '') static 通过回调方法验证某个字段是否必须
* @method ValidateRule requireWith(mixed $rule, string $msg = '') static 验证某个字段有值的情况下必须
* @method ValidateRule must(mixed $rule = null, string $msg = '') static 必须验证
*/
class ValidateRule
{
// 验证字段的名称
protected $title;
// 当前验证规则
protected $rule = [];
// 验证提示信息
protected $message = [];
/**
* 添加验证因子
* @access protected
* @param string $name 验证名称
* @param mixed $rule 验证规则
* @param string $msg 提示信息
* @return $this
*/
protected function addItem(string $name, $rule = null, string $msg = '')
{
if ($rule || 0 === $rule) {
$this->rule[$name] = $rule;
} else {
$this->rule[] = $name;
}
$this->message[] = $msg;
return $this;
}
/**
* 获取验证规则
* @access public
* @return array
*/
public function getRule(): array
{
return $this->rule;
}
/**
* 获取验证字段名称
* @access public
* @return string
*/
public function getTitle(): string
{
return $this->title ?: '';
}
/**
* 获取验证提示
* @access public
* @return array
*/
public function getMsg(): array
{
return $this->message;
}
/**
* 设置验证字段名称
* @access public
* @return $this
*/
public function title(string $title)
{
$this->title = $title;
return $this;
}
public function __call($method, $args)
{
if ('is' == strtolower(substr($method, 0, 2))) {
$method = substr($method, 2);
}
array_unshift($args, lcfirst($method));
return call_user_func_array([$this, 'addItem'], $args);
}
public static function __callStatic($method, $args)
{
$rule = new static();
if ('is' == strtolower(substr($method, 0, 2))) {
$method = substr($method, 2);
}
array_unshift($args, lcfirst($method));
return call_user_func_array([$rule, 'addItem'], $args);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment