diff --git a/composer.json b/composer.json index 038f950..d4fc7b8 100644 --- a/composer.json +++ b/composer.json @@ -17,12 +17,17 @@ "pimple/pimple": "^3.0", "symfony/cache": "^3.3 || ^4.0", "symfony/http-foundation": "^2.7 || ^3.0 || ^4.0", - "symfony/psr-http-message-bridge": "^0.3 || ^1.0" + "symfony/psr-http-message-bridge": "^0.3 || ^1.0", + "symfony/event-dispatcher": "^5.0", + "monolog/monolog": "^1.22 || ^2.0" }, "autoload": { "psr-4": { "EasyAlipay\\": "src/" - } + }, + "files": [ + "src/Kernel/Helpers.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/src/Factory.php b/src/Factory.php index 946f4cd..3367b49 100755 --- a/src/Factory.php +++ b/src/Factory.php @@ -9,7 +9,7 @@ namespace EasyAlipay; * @method static \EasyAlipay\Mini\Application mini(array $config) * @method static \EasyAlipay\OpenPublic\Application openPublic(array $config) * @method static \EasyAlipay\Marketing\Application marketing(array $config) - * @method static \EasyAlipay\BasicService\Application basicService(array $config) + * @method static \EasyAlipay\Base\Application base(array $config) */ class Factory { diff --git a/src/Kernel/AopClient.php b/src/Kernel/AopClient.php index 1644481..1faff67 100755 --- a/src/Kernel/AopClient.php +++ b/src/Kernel/AopClient.php @@ -2,10 +2,12 @@ namespace EasyAlipay\Kernel; +use EasyAlipay\Kernel\Exceptions\InvalidConfigException; +use function EasyAlipay\Kernel\encrypt; use Exception; use EasyAlipay\Kernel\SignData; -class AopClient +class AopClient extends BaseClient { //应用ID private $appId; @@ -48,35 +50,46 @@ class AopClient protected $alipaySdkVersion = "alipay-sdk-php-easyalipay-20190820"; - public function __construct(ServiceContainer $app){ + public function __construct(ServiceContainer $app) + { + parent::__construct($app); + $this->appId = $app['config']['app_id']; $this->rsaPrivateKey = $app['config']['merchant_private_key']; $this->alipayrsaPublicKey = $app['config']['alipay_public_key']; $this->postCharset = $app['config']['charset']; - $this->signType=$app['config']['sign_type']; + $this->signType = $app['config']['sign_type']; $this->gatewayUrl = $app['config']['gateway_url']; - if(empty($this->appId)||trim($this->appId)==""){ - throw new Exception("appId should not be NULL!"); + if (empty($this->appId) || trim($this->appId) == "") { + throw new InvalidConfigException("appId should not be NULL!"); } - if(empty($this->rsaPrivateKey)||trim($this->rsaPrivateKey)==""){ - throw new Exception("rsaPrivateKey should not be NULL!"); + if (empty($this->rsaPrivateKey) || trim($this->rsaPrivateKey) == "") { + throw new InvalidConfigException("rsaPrivateKey should not be NULL!"); } - if(empty($this->alipayrsaPublicKey)||trim($this->alipayrsaPublicKey)==""){ - throw new Exception("alipayPublicKey should not be NULL!"); + if (empty($this->alipayrsaPublicKey) || trim($this->alipayrsaPublicKey) == "") { + throw new InvalidConfigException("alipayPublicKey should not be NULL!"); } - if(empty($this->postCharset)||trim($this->postCharset)==""){ - throw new Exception("postCharset should not be NULL!"); + if (empty($this->postCharset) || trim($this->postCharset) == "") { + throw new InvalidConfigException("postCharset should not be NULL!"); } - if(empty($this->signType)||trim($this->signType)==""){ - throw new Exception("signType should not be NULL!"); + if (empty($this->signType) || trim($this->signType) == "") { + throw new InvalidConfigException("signType should not be NULL!"); } - if(empty($this->gatewayUrl)||trim($this->gatewayUrl)==""){ - throw new Exception("gatewayUrl should not be NULL!"); + if (empty($this->gatewayUrl) || trim($this->gatewayUrl) == "") { + throw new InvalidConfigException("gatewayUrl should not be NULL!"); } } - public function execute($request, $authToken = null, $appInfoAuthtoken = null) { + /** + * @param AopRequest $request + * @param null $authToken + * @param null $appInfoAuthtoken + * @return bool|mixed|\SimpleXMLElement + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function execute(AopRequest $request, $authToken = null, $appInfoAuthtoken = null) + { $this->setupCharsets($request); //如果两者编码不一致,会出现签名验签或者乱码 if (strcasecmp($this->fileCharset, $this->postCharset)) { @@ -106,7 +119,7 @@ class AopClient $sysParams["app_auth_token"] = $appInfoAuthtoken; //获取业务参数 $apiParams = $request->getApiParas(); - if (method_exists($request,"getNeedEncrypt") &&$request->getNeedEncrypt()){ + if (method_exists($request, "getNeedEncrypt") && $request->getNeedEncrypt()) { $sysParams["encrypt_type"] = $this->encryptType; if ($this->checkEmpty($apiParams['biz_content'])) { throw new Exception(" api request Fail! The reason : encrypt request is not supperted!"); @@ -131,47 +144,47 @@ class AopClient $requestUrl = substr($requestUrl, 0, -1); //发起HTTP请求 try { - $resp = $this->curl($requestUrl, $apiParams); + $resp = $this->httpPost($requestUrl, $apiParams); + var_dump($resp);die; } catch (Exception $e) { - var_dump("HTTP_ERROR_" . $e->getCode(), $e->getMessage()); + var_dump($e->getMessage());die; return false; } //解析AOP返回结果 $respWellFormed = false; // 将返回结果转换本地文件编码 - $r = iconv($this->postCharset, $this->fileCharset . "//IGNORE", $resp); +// $r = iconv($this->postCharset, $this->fileCharset . "//IGNORE", $resp); $signData = null; if ("json" == $this->format) { - $respObject = json_decode($r); + $respObject = $resp; if (null !== $respObject) { $respWellFormed = true; $signData = $this->parserJSONSignData($request, $resp, $respObject); + var_dump($signData);die; } } else if ("xml" == $this->format) { $disableLibxmlEntityLoader = libxml_disable_entity_loader(true); $respObject = @ simplexml_load_string($resp); if (false !== $respObject) { $respWellFormed = true; - $signData = $this->parserXMLSignData($request, $resp); } libxml_disable_entity_loader($disableLibxmlEntityLoader); } //返回的HTTP文本不是标准JSON或者XML,记下错误日志 if (false === $respWellFormed) { - var_dump("HTTP_RESPONSE_NOT_WELL_FORMED_".$resp); return false; } // 验签 $this->checkResponseSign($request, $signData, $resp, $respObject); // 解密 - if (method_exists($request,"getNeedEncrypt") &&$request->getNeedEncrypt()){ + if (method_exists($request, "getNeedEncrypt") && $request->getNeedEncrypt()) { if ("json" == $this->format) { $resp = $this->encryptJSONSignSource($request, $resp); // 将返回结果转换本地文件编码 $r = iconv($this->postCharset, $this->fileCharset . "//IGNORE", $resp); $respObject = json_decode($r); - }else{ + } else { $resp = $this->encryptXMLSignSource($request, $resp); $r = iconv($this->postCharset, $this->fileCharset . "//IGNORE", $resp); $disableLibxmlEntityLoader = libxml_disable_entity_loader(true); @@ -183,116 +196,13 @@ class AopClient } - /** - * 生成用于调用收银台SDK的字符串 - * @param $request SDK接口的请求参数对象 - * @param $appAuthToken 三方应用授权token - * @return string - * @author guofa.tgf - */ - public function sdkExecute($request, $appAuthToken = null) { - $this->setupCharsets($request); - $params['app_id'] = $this->appId; - $params['method'] = $request->getApiMethodName(); - $params['format'] = $this->format; - $params['sign_type'] = $this->signType; - $params['timestamp'] = date("Y-m-d H:i:s"); - $params['alipay_sdk'] = $this->alipaySdkVersion; - $params['charset'] = $this->postCharset; - $version = $request->getApiVersion(); - $params['version'] = $this->checkEmpty($version) ? $this->apiVersion : $version; - if ($notify_url = $request->getNotifyUrl()) { - $params['notify_url'] = $notify_url; - } - $params['app_auth_token'] = $appAuthToken; - $dict = $request->getApiParas(); - $params['biz_content'] = $dict['biz_content']; - ksort($params); - $params['sign'] = $this->generateSign($params, $this->signType); - foreach ($params as &$value) { - $value = $this->characet($value, $params['charset']); - } - return http_build_query($params); - } - - /** - * 页面提交执行方法 - * @param $request 跳转类接口的request - * @param string $httpmethod 提交方式,两个值可选:post、get; - * @param null $appAuthToken 三方应用授权token - * @return 构建好的、签名后的最终跳转URL(GET)或String形式的form(POST) - * @throws Exception - */ - public function pageExecute($request, $httpmethod = "POST", $appAuthToken = null) { - $this->setupCharsets($request); - if (strcasecmp($this->fileCharset, $this->postCharset)) { - // writeLog("本地文件字符集编码与表单提交编码不一致,请务必设置成一样,属性名分别为postCharset!"); - throw new Exception("文件编码:[" . $this->fileCharset . "] 与表单提交编码:[" . $this->postCharset . "]两者不一致!"); - } - $iv=null; - if(!$this->checkEmpty($request->getApiVersion())){ - $iv=$request->getApiVersion(); - }else{ - $iv=$this->apiVersion; - } - //组装系统参数 - $sysParams["app_id"] = $this->appId; - $sysParams["version"] = $iv; - $sysParams["format"] = $this->format; - $sysParams["sign_type"] = $this->signType; - $sysParams["method"] = $request->getApiMethodName(); - $sysParams["timestamp"] = date("Y-m-d H:i:s"); - $sysParams["alipay_sdk"] = $this->alipaySdkVersion; - $sysParams["terminal_type"] = $request->getTerminalType(); - $sysParams["terminal_info"] = $request->getTerminalInfo(); - $sysParams["prod_code"] = $request->getProdCode(); - $sysParams["notify_url"] = $request->getNotifyUrl(); - $sysParams["return_url"] = $request->getReturnUrl(); - $sysParams["charset"] = $this->postCharset; - $sysParams["app_auth_token"] = $appAuthToken; - //获取业务参数 - $apiParams = $request->getApiParas(); - if (method_exists($request,"getNeedEncrypt") &&$request->getNeedEncrypt()){ - $sysParams["encrypt_type"] = $this->encryptType; - if ($this->checkEmpty($apiParams['biz_content'])) { - throw new Exception(" api request Fail! The reason : encrypt request is not supperted!"); - } - if ($this->checkEmpty($this->encryptKey) || $this->checkEmpty($this->encryptType)) { - throw new Exception(" encryptType and encryptKey must not null! "); - } - if ("AES" != $this->encryptType) { - throw new Exception("加密类型只支持AES"); - } - // 执行加密 - $enCryptContent = encrypt($apiParams['biz_content'], $this->encryptKey); - $apiParams['biz_content'] = $enCryptContent; - } - $totalParams = array_merge($apiParams, $sysParams); - //待签名字符串 - $preSignStr = $this->getSignContent($totalParams); - //签名 - $totalParams["sign"] = $this->generateSign($totalParams, $this->signType); - if ("GET" == strtoupper($httpmethod)) { - //value做urlencode - $preString=$this->getSignContentUrlencode($totalParams); - //拼接GET请求串 - $requestUrl = $this->gatewayUrl."?".$preString; - return $requestUrl; - } else { - //拼接表单字符串 - return $this->buildRequestForm($totalParams); - } - } - - public function generateSign($params, $signType = "RSA") { - return $this->sign($this->getSignContent($params), $signType); - } - - public function rsaSign($params, $signType = "RSA") { + public function generateSign($params, $signType = "RSA") + { return $this->sign($this->getSignContent($params), $signType); } - public function getSignContent($params) { + public function getSignContent($params) + { ksort($params); $stringToBeSigned = ""; $i = 0; @@ -313,78 +223,24 @@ class AopClient return $stringToBeSigned; } - - //此方法对value做urlencode - public function getSignContentUrlencode($params) { - ksort($params); - $stringToBeSigned = ""; - $i = 0; - foreach ($params as $k => $v) { - if (false === $this->checkEmpty($v) && "@" != substr($v, 0, 1)) { - // 转换成目标字符集 - $v = $this->characet($v, $this->postCharset); - if ($i == 0) { - $stringToBeSigned .= "$k" . "=" . urlencode($v); - } else { - $stringToBeSigned .= "&" . "$k" . "=" . urlencode($v); - } - $i++; - } - } - unset ($k, $v); - return $stringToBeSigned; - } - - protected function sign($data, $signType = "RSA") { - if($this->checkEmpty($this->rsaPrivateKeyFilePath)){ - $priKey=$this->rsaPrivateKey; + protected function sign($data, $signType = "RSA") + { + if ($this->checkEmpty($this->rsaPrivateKeyFilePath)) { + $priKey = $this->rsaPrivateKey; $res = "-----BEGIN RSA PRIVATE KEY-----\n" . wordwrap($priKey, 64, "\n", true) . "\n-----END RSA PRIVATE KEY-----"; - }else { - $priKey = file_get_contents($this->rsaPrivateKeyFilePath); - $res = openssl_get_privatekey($priKey); - } - ($res) or die('您使用的私钥格式错误,请检查RSA私钥配置'); - if ("RSA2" == $signType) { - openssl_sign($data, $sign, $res, OPENSSL_ALGO_SHA256); } else { - openssl_sign($data, $sign, $res); - } - if(!$this->checkEmpty($this->rsaPrivateKeyFilePath)){ - openssl_free_key($res); - } - $sign = base64_encode($sign); - return $sign; - } - - /** - * RSA单独签名方法,未做字符串处理,字符串处理见getSignContent() - * @param $data 待签名字符串 - * @param $privatekey 商户私钥,根据keyfromfile来判断是读取字符串还是读取文件,false:填写私钥字符串去回车和空格 true:填写私钥文件路径 - * @param $signType 签名方式,RSA:SHA1 RSA2:SHA256 - * @param $keyfromfile 私钥获取方式,读取字符串还是读文件 - * @return string - * @author mengyu.wh - */ - public function alonersaSign($data,$privatekey,$signType = "RSA",$keyfromfile=false) { - if(!$keyfromfile){ - $priKey=$privatekey; - $res = "-----BEGIN RSA PRIVATE KEY-----\n" . - wordwrap($priKey, 64, "\n", true) . - "\n-----END RSA PRIVATE KEY-----"; - } - else{ - $priKey = file_get_contents($privatekey); + $priKey = file_get_contents($this->rsaPrivateKeyFilePath); $res = openssl_get_privatekey($priKey); } - ($res) or die('您使用的私钥格式错误,请检查RSA私钥配置'); + ($res) or die('您使用的私钥格式错误,请检查RSA私钥配置'); if ("RSA2" == $signType) { openssl_sign($data, $sign, $res, OPENSSL_ALGO_SHA256); } else { openssl_sign($data, $sign, $res); } - if($keyfromfile){ + if (!$this->checkEmpty($this->rsaPrivateKeyFilePath)) { openssl_free_key($res); } $sign = base64_encode($sign); @@ -392,7 +248,8 @@ class AopClient } - protected function curl($url, $postFields = null) { + protected function curl($url, $postFields = null) + { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_FAILONERROR, false); @@ -440,61 +297,20 @@ class AopClient return $reponse; } - protected function getMillisecond() { + protected function getMillisecond() + { list($s1, $s2) = explode(' ', microtime()); return (float)sprintf('%.0f', (floatval($s1) + floatval($s2)) * 1000); } - - protected function logCommunicationError($apiName, $requestUrl, $errorCode, $responseTxt) { - $localIp = isset ($_SERVER["SERVER_ADDR"]) ? $_SERVER["SERVER_ADDR"] : "CLI"; - $logger = new LtLogger; - $logger->conf["log_file"] = rtrim(AOP_SDK_WORK_DIR, '\\/') . '/' . "logs/aop_comm_err_" . $this->appId . "_" . date("Y-m-d") . ".log"; - $logger->conf["separator"] = "^_^"; - $logData = array( - date("Y-m-d H:i:s"), - $apiName, - $this->appId, - $localIp, - PHP_OS, - $this->alipaySdkVersion, - $requestUrl, - $errorCode, - str_replace("\n", "", $responseTxt) - ); - $logger->log($logData); - } - - - /** - * 建立请求,以表单HTML形式构造(默认) - * @param $para_temp 请求参数数组 - * @return 提交表单HTML文本 - */ - protected function buildRequestForm($para_temp) { - $sHtml = "
"; - while (list ($key, $val) = each ($para_temp)) { - if (false === $this->checkEmpty($val)) { - //$val = $this->characet($val, $this->postCharset); - $val = str_replace("'","'",$val); - //$val = str_replace("\"",""",$val); - $sHtml.= ""; - } - } - //submit按钮控件请不要含有name属性 - $sHtml = $sHtml."
"; - $sHtml = $sHtml.""; - return $sHtml; - } - - /** * 转换字符集编码 * @param $data * @param $targetCharset * @return string */ - function characet($data, $targetCharset) { + function characet($data, $targetCharset) + { if (!empty($data)) { $fileType = $this->fileCharset; if (strcasecmp($fileType, $targetCharset) != 0) { @@ -505,36 +321,13 @@ class AopClient return $data; } - public function exec($paramsArray) { - if (!isset ($paramsArray["method"])) { - trigger_error("No api name passed"); - } - $inflector = new LtInflector; - $inflector->conf["separator"] = "."; - $requestClassName = ucfirst($inflector->camelize(substr($paramsArray["method"], 7))) . "Request"; - if (!class_exists($requestClassName)) { - trigger_error("No such api: " . $paramsArray["method"]); - } - $session = isset ($paramsArray["session"]) ? $paramsArray["session"] : null; - $req = new $requestClassName; - foreach ($paramsArray as $paraKey => $paraValue) { - $inflector->conf["separator"] = "_"; - $setterMethodName = $inflector->camelize($paraKey); - $inflector->conf["separator"] = "."; - $setterMethodName = "set" . $inflector->camelize($setterMethodName); - if (method_exists($req, $setterMethodName)) { - $req->$setterMethodName ($paraValue); - } - } - return $this->execute($req, $session); - } - /** * 校验$value是否非空 * if not set ,return true; * if is null , return true; **/ - protected function checkEmpty($value) { + protected function checkEmpty($value) + { if (!isset($value)) return true; if ($value === null) @@ -544,184 +337,45 @@ class AopClient return false; } - /** rsaCheckV1 & rsaCheckV2 - * 验证签名 - * 在使用本方法前,必须初始化AopClient且传入公钥参数。 - * 公钥是否是读取字符串还是读取文件,是根据初始化传入的值判断的。 - **/ - public function rsaCheckV1($params, $rsaPublicKeyFilePath,$signType='RSA') { - $sign = $params['sign']; - $params['sign_type'] = null; - $params['sign'] = null; - return $this->verify($this->getSignContent($params), $sign, $rsaPublicKeyFilePath,$signType); - } - public function rsaCheckV2($params, $rsaPublicKeyFilePath, $signType='RSA') { + + public function rsaCheckV2($params, $rsaPublicKeyFilePath, $signType = 'RSA') + { $sign = $params['sign']; $params['sign'] = null; return $this->verify($this->getSignContent($params), $sign, $rsaPublicKeyFilePath, $signType); } - function verify($data, $sign, $rsaPublicKeyFilePath, $signType = 'RSA') { - if($this->checkEmpty($this->alipayPublicKey)){ - $pubKey= $this->alipayrsaPublicKey; + function verify($data, $sign, $rsaPublicKeyFilePath, $signType = 'RSA') + { + if ($this->checkEmpty($this->alipayPublicKey)) { + $pubKey = $this->alipayrsaPublicKey; $res = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($pubKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----"; - }else { + } else { //读取公钥文件 $pubKey = file_get_contents($rsaPublicKeyFilePath); //转换为openssl格式密钥 $res = openssl_get_publickey($pubKey); } - ($res) or die('支付宝RSA公钥错误。请检查公钥文件格式是否正确'); + ($res) or die('支付宝RSA公钥错误。请检查公钥文件格式是否正确'); //调用openssl内置方法验签,返回bool值 $result = FALSE; if ("RSA2" == $signType) { - $result = (openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256)===1); + $result = (openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256) === 1); } else { - $result = (openssl_verify($data, base64_decode($sign), $res)===1); + $result = (openssl_verify($data, base64_decode($sign), $res) === 1); } - if(!$this->checkEmpty($this->alipayPublicKey)) { + if (!$this->checkEmpty($this->alipayPublicKey)) { //释放资源 openssl_free_key($res); } return $result; } -/** - * 在使用本方法前,必须初始化AopClient且传入公私钥参数。 - * 公钥是否是读取字符串还是读取文件,是根据初始化传入的值判断的。 - **/ - public function checkSignAndDecrypt($params, $rsaPublicKeyPem, $rsaPrivateKeyPem, $isCheckSign, $isDecrypt, $signType='RSA') { - $charset = $params['charset']; - $bizContent = $params['biz_content']; - if ($isCheckSign) { - if (!$this->rsaCheckV2($params, $rsaPublicKeyPem, $signType)) { - echo "
checkSign failure
"; - exit; - } - } - if ($isDecrypt) { - return $this->rsaDecrypt($bizContent, $rsaPrivateKeyPem, $charset); - } - return $bizContent; - } - - /** - * 在使用本方法前,必须初始化AopClient且传入公私钥参数。 - * 公钥是否是读取字符串还是读取文件,是根据初始化传入的值判断的。 - **/ - public function encryptAndSign($bizContent, $rsaPublicKeyPem, $rsaPrivateKeyPem, $charset, $isEncrypt, $isSign, $signType='RSA') { - // 加密,并签名 - if ($isEncrypt && $isSign) { - $encrypted = $this->rsaEncrypt($bizContent, $rsaPublicKeyPem, $charset); - $sign = $this->sign($encrypted, $signType); - $response = "$encryptedRSA$sign$signType"; - return $response; - } - // 加密,不签名 - if ($isEncrypt && (!$isSign)) { - $encrypted = $this->rsaEncrypt($bizContent, $rsaPublicKeyPem, $charset); - $response = "$encrypted$signType"; - return $response; - } - // 不加密,但签名 - if ((!$isEncrypt) && $isSign) { - $sign = $this->sign($bizContent, $signType); - $response = "$bizContent$sign$signType"; - return $response; - } - // 不加密,不签名 - $response = "$bizContent"; - return $response; - } - - /** - * 在使用本方法前,必须初始化AopClient且传入公私钥参数。 - * 公钥是否是读取字符串还是读取文件,是根据初始化传入的值判断的。 - **/ - public function rsaEncrypt($data, $rsaPublicKeyPem, $charset) { - if($this->checkEmpty($this->alipayPublicKey)){ - //读取字符串 - $pubKey= $this->alipayrsaPublicKey; - $res = "-----BEGIN PUBLIC KEY-----\n" . - wordwrap($pubKey, 64, "\n", true) . - "\n-----END PUBLIC KEY-----"; - }else { - //读取公钥文件 - $pubKey = file_get_contents($rsaPublicKeyFilePath); - //转换为openssl格式密钥 - $res = openssl_get_publickey($pubKey); - } - ($res) or die('支付宝RSA公钥错误。请检查公钥文件格式是否正确'); - $blocks = $this->splitCN($data, 0, 30, $charset); - $chrtext  = null; - $encodes  = array(); - foreach ($blocks as $n => $block) { - if (!openssl_public_encrypt($block, $chrtext , $res)) { - echo "
" . openssl_error_string() . "
"; - } - $encodes[] = $chrtext ; - } - $chrtext = implode(",", $encodes); - return base64_encode($chrtext); - } - - /** - * 在使用本方法前,必须初始化AopClient且传入公私钥参数。 - * 公钥是否是读取字符串还是读取文件,是根据初始化传入的值判断的。 - **/ - public function rsaDecrypt($data, $rsaPrivateKeyPem, $charset) { - if($this->checkEmpty($this->rsaPrivateKeyFilePath)){ - //读字符串 - $priKey=$this->rsaPrivateKey; - $res = "-----BEGIN RSA PRIVATE KEY-----\n" . - wordwrap($priKey, 64, "\n", true) . - "\n-----END RSA PRIVATE KEY-----"; - }else { - $priKey = file_get_contents($this->rsaPrivateKeyFilePath); - $res = openssl_get_privatekey($priKey); - } - ($res) or die('您使用的私钥格式错误,请检查RSA私钥配置'); - //转换为openssl格式密钥 - $decodes = explode(',', $data); - $strnull = ""; - $dcyCont = ""; - foreach ($decodes as $n => $decode) { - if (!openssl_private_decrypt($decode, $dcyCont, $res)) { - echo "
" . openssl_error_string() . "
"; - } - $strnull .= $dcyCont; - } - return $strnull; - } - - function splitCN($cont, $n = 0, $subnum, $charset) { - //$len = strlen($cont) / 3; - $arrr = array(); - for ($i = $n; $i < strlen($cont); $i += $subnum) { - $res = $this->subCNchar($cont, $i, $subnum, $charset); - if (!empty ($res)) { - $arrr[] = $res; - } - } - return $arrr; - } - - function subCNchar($str, $start = 0, $length, $charset = "gbk") { - if (strlen($str) <= $length) { - return $str; - } - $re['utf-8'] = "/[\x01-\x7f]|[\xc2-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xff][\x80-\xbf]{3}/"; - $re['gb2312'] = "/[\x01-\x7f]|[\xb0-\xf7][\xa0-\xfe]/"; - $re['gbk'] = "/[\x01-\x7f]|[\x81-\xfe][\x40-\xfe]/"; - $re['big5'] = "/[\x01-\x7f]|[\x81-\xfe]([\x40-\x7e]|\xa1-\xfe])/"; - preg_match_all($re[$charset], $str, $match); - $slice = join("", array_slice($match[0], $start, $length)); - return $slice; - } - function parserResponseSubCode($request, $responseContent, $respObject, $format) { + function parserResponseSubCode($request, $responseContent, $respObject, $format) + { if ("json" == $format) { $apiName = $request->getApiMethodName(); $rootNodeName = str_replace(".", "_", $apiName) . $this->RESPONSE_SUFFIX; @@ -748,14 +402,16 @@ class AopClient } } - function parserJSONSignData($request, $responseContent, $responseJSON) { + function parserJSONSignData($request, $responseContent, $responseJSON) + { $signData = new SignData(); $signData->sign = $this->parserJSONSign($responseJSON); $signData->signSourceData = $this->parserJSONSignSource($request, $responseContent); return $signData; } - function parserJSONSignSource($request, $responseContent) { + function parserJSONSignSource($request, $responseContent) + { $apiName = $request->getApiMethodName(); $rootNodeName = str_replace(".", "_", $apiName) . $this->RESPONSE_SUFFIX; $rootIndex = strpos($responseContent, $rootNodeName); @@ -769,7 +425,8 @@ class AopClient } } - function parserJSONSource($responseContent, $nodeName, $nodeIndex) { + function parserJSONSource($responseContent, $nodeName, $nodeIndex) + { $signDataStartIndex = $nodeIndex + strlen($nodeName) + 2; $signIndex = strrpos($responseContent, "\"" . $this->SIGN_NODE_NAME . "\""); // 签名前-逗号 @@ -781,18 +438,21 @@ class AopClient return substr($responseContent, $signDataStartIndex, $indexLen); } - function parserJSONSign($responseJSon) { + function parserJSONSign($responseJSon) + { return $responseJSon->sign; } - function parserXMLSignData($request, $responseContent) { + function parserXMLSignData($request, $responseContent) + { $signData = new SignData(); $signData->sign = $this->parserXMLSign($responseContent); $signData->signSourceData = $this->parserXMLSignSource($request, $responseContent); return $signData; } - function parserXMLSignSource($request, $responseContent) { + function parserXMLSignSource($request, $responseContent) + { $apiName = $request->getApiMethodName(); $rootNodeName = str_replace(".", "_", $apiName) . $this->RESPONSE_SUFFIX; $rootIndex = strpos($responseContent, $rootNodeName); @@ -806,7 +466,8 @@ class AopClient } } - function parserXMLSource($responseContent, $nodeName, $nodeIndex) { + function parserXMLSource($responseContent, $nodeName, $nodeIndex) + { $signDataStartIndex = $nodeIndex + strlen($nodeName) + 1; $signIndex = strrpos($responseContent, "<" . $this->SIGN_NODE_NAME . ">"); // 签名前-逗号 @@ -818,7 +479,8 @@ class AopClient return substr($responseContent, $signDataStartIndex, $indexLen); } - function parserXMLSign($responseContent) { + function parserXMLSign($responseContent) + { $signNodeName = "<" . $this->SIGN_NODE_NAME . ">"; $signEndNodeName = "SIGN_NODE_NAME . ">"; $indexOfSignNode = strpos($responseContent, $signNodeName); @@ -843,7 +505,8 @@ class AopClient * @param $respObject * @throws Exception */ - public function checkResponseSign($request, $signData, $resp, $respObject) { + public function checkResponseSign(AopRequest $request, $signData, $resp, $respObject) + { if (!$this->checkEmpty($this->alipayPublicKey) || !$this->checkEmpty($this->alipayrsaPublicKey)) { if ($signData == null || $this->checkEmpty($signData->sign) || $this->checkEmpty($signData->signSourceData)) { throw new Exception(" check sign Fail! The reason : signData is Empty"); @@ -867,7 +530,8 @@ class AopClient } } - private function setupCharsets($request) { + private function setupCharsets($request) + { if ($this->checkEmpty($this->postCharset)) { $this->postCharset = 'UTF-8'; } @@ -877,7 +541,8 @@ class AopClient // 获取加密内容 - private function encryptJSONSignSource($request, $responseContent) { + private function encryptJSONSignSource($request, $responseContent) + { $parsetItem = $this->parserEncryptJSONSignSource($request, $responseContent); $bodyIndexContent = substr($responseContent, 0, $parsetItem->startIndex); $bodyEndContent = substr($responseContent, $parsetItem->endIndex, strlen($responseContent) + 1 - $parsetItem->endIndex); @@ -886,7 +551,8 @@ class AopClient } - private function parserEncryptJSONSignSource($request, $responseContent) { + private function parserEncryptJSONSignSource($request, $responseContent) + { $apiName = $request->getApiMethodName(); $rootNodeName = str_replace(".", "_", $apiName) . $this->RESPONSE_SUFFIX; $rootIndex = strpos($responseContent, $rootNodeName); @@ -900,16 +566,17 @@ class AopClient } } - private function parserEncryptJSONItem($responseContent, $nodeName, $nodeIndex) { + private function parserEncryptJSONItem($responseContent, $nodeName, $nodeIndex) + { $signDataStartIndex = $nodeIndex + strlen($nodeName) + 2; $signIndex = strpos($responseContent, "\"" . $this->SIGN_NODE_NAME . "\""); // 签名前-逗号 $signDataEndIndex = $signIndex - 1; if ($signDataEndIndex < 0) { - $signDataEndIndex = strlen($responseContent)-1 ; + $signDataEndIndex = strlen($responseContent) - 1; } $indexLen = $signDataEndIndex - $signDataStartIndex; - $encContent = substr($responseContent, $signDataStartIndex+1, $indexLen-2); + $encContent = substr($responseContent, $signDataStartIndex + 1, $indexLen - 2); $encryptParseItem = new EncryptParseItem(); $encryptParseItem->encryptContent = $encContent; $encryptParseItem->startIndex = $signDataStartIndex; @@ -919,7 +586,8 @@ class AopClient // 获取加密内容 - private function encryptXMLSignSource($request, $responseContent) { + private function encryptXMLSignSource($request, $responseContent) + { $parsetItem = $this->parserEncryptXMLSignSource($request, $responseContent); $bodyIndexContent = substr($responseContent, 0, $parsetItem->startIndex); $bodyEndContent = substr($responseContent, $parsetItem->endIndex, strlen($responseContent) + 1 - $parsetItem->endIndex); @@ -927,7 +595,8 @@ class AopClient return $bodyIndexContent . $bizContent . $bodyEndContent; } - private function parserEncryptXMLSignSource($request, $responseContent) { + private function parserEncryptXMLSignSource($request, $responseContent) + { $apiName = $request->getApiMethodName(); $rootNodeName = str_replace(".", "_", $apiName) . $this->RESPONSE_SUFFIX; $rootIndex = strpos($responseContent, $rootNodeName); @@ -942,29 +611,31 @@ class AopClient } } - private function parserEncryptXMLItem($responseContent, $nodeName, $nodeIndex) { + private function parserEncryptXMLItem($responseContent, $nodeName, $nodeIndex) + { $signDataStartIndex = $nodeIndex + strlen($nodeName) + 1; - $xmlStartNode="<".$this->ENCRYPT_XML_NODE_NAME.">"; - $xmlEndNode="ENCRYPT_XML_NODE_NAME.">"; - $indexOfXmlNode=strpos($responseContent,$xmlEndNode); - if($indexOfXmlNode<0){ + $xmlStartNode = "<" . $this->ENCRYPT_XML_NODE_NAME . ">"; + $xmlEndNode = "ENCRYPT_XML_NODE_NAME . ">"; + $indexOfXmlNode = strpos($responseContent, $xmlEndNode); + if ($indexOfXmlNode < 0) { $item = new EncryptParseItem(); $item->encryptContent = null; $item->startIndex = 0; $item->endIndex = 0; return $item; } - $startIndex=$signDataStartIndex+strlen($xmlStartNode); - $bizContentLen=$indexOfXmlNode-$startIndex; - $bizContent=substr($responseContent,$startIndex,$bizContentLen); + $startIndex = $signDataStartIndex + strlen($xmlStartNode); + $bizContentLen = $indexOfXmlNode - $startIndex; + $bizContent = substr($responseContent, $startIndex, $bizContentLen); $encryptParseItem = new EncryptParseItem(); $encryptParseItem->encryptContent = $bizContent; $encryptParseItem->startIndex = $signDataStartIndex; - $encryptParseItem->endIndex = $indexOfXmlNode+strlen($xmlEndNode); + $encryptParseItem->endIndex = $indexOfXmlNode + strlen($xmlEndNode); return $encryptParseItem; } - function echoDebug($content) { + function echoDebug($content) + { if ($this->debugInfo) { echo "
" . $content; } diff --git a/src/Kernel/BaseClient.php b/src/Kernel/BaseClient.php new file mode 100644 index 0000000..4da1c4d --- /dev/null +++ b/src/Kernel/BaseClient.php @@ -0,0 +1,216 @@ +app = $app; + } + + /** + * GET request. + * + * @param string $url + * @param array $query + * + * @return \Psr\Http\Message\ResponseInterface|\EasyAlipay\Kernel\Support\Collection|array|object|string + * + * @throws \EasyAlipay\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function httpGet(string $url, array $query = []) + { + return $this->request($url, 'GET', ['query' => $query]); + } + + /** + * POST request. + * + * @param string $url + * @param array $data + * + * @return \Psr\Http\Message\ResponseInterface|\EasyAlipay\Kernel\Support\Collection|array|object|string + * + * @throws \EasyAlipay\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function httpPost(string $url, array $data = []) + { + return $this->request($url, 'POST', ['form_params' => $data]); + } + + /** + * JSON request. + * + * @param string $url + * @param array $data + * @param array $query + * + * @return \Psr\Http\Message\ResponseInterface|\EasyAlipay\Kernel\Support\Collection|array|object|string + * + * @throws \EasyAlipay\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function httpPostJson(string $url, array $data = [], array $query = []) + { + return $this->request($url, 'POST', ['query' => $query, 'json' => $data]); + } + + /** + * Upload file. + * + * @param string $url + * @param array $files + * @param array $form + * @param array $query + * + * @return \Psr\Http\Message\ResponseInterface|\EasyAlipay\Kernel\Support\Collection|array|object|string + * + * @throws \EasyAlipay\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function httpUpload(string $url, array $files = [], array $form = [], array $query = []) + { + $multipart = []; + + foreach ($files as $name => $path) { + $multipart[] = [ + 'name' => $name, + 'contents' => fopen($path, 'r'), + ]; + } + + foreach ($form as $name => $contents) { + $multipart[] = compact('name', 'contents'); + } + + return $this->request($url, 'POST', ['query' => $query, 'multipart' => $multipart, 'connect_timeout' => 30, 'timeout' => 30, 'read_timeout' => 30]); + } + + /** + * @param string $url + * @param string $method + * @param array $options + * @param bool $returnRaw + * + * @return \Psr\Http\Message\ResponseInterface|\EasyAlipay\Kernel\Support\Collection|array|object|string + * + * @throws \EasyAlipay\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function request(string $url, string $method = 'GET', array $options = [], $returnRaw = false) + { + if (empty($this->middlewares)) { + $this->registerHttpMiddlewares(); + } + + $response = $this->performRequest($url, $method, $options); + + $this->app->events->dispatch(new Events\HttpResponseCreated($response)); + + return $returnRaw ? $response : $this->castResponseToType($response, $this->app->config->get('response_type')); + } + + /** + * @param string $url + * @param string $method + * @param array $options + * + * @return \EasyAlipay\Kernel\Http\Response + * + * @throws \EasyAlipay\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function requestRaw(string $url, string $method = 'GET', array $options = []) + { + return Response::buildFromPsrResponse($this->request($url, $method, $options, true)); + } + + /** + * Register Guzzle middlewares. + */ + protected function registerHttpMiddlewares() + { + // retry + $this->pushMiddleware($this->retryMiddleware(), 'retry'); + // log + $this->pushMiddleware($this->logMiddleware(), 'log'); + } + + /** + * Log the request. + * + * @return \Closure + */ + protected function logMiddleware() + { + $formatter = new MessageFormatter($this->app['config']['http.log_template'] ?? MessageFormatter::DEBUG); + + return Middleware::log($this->app['logger'], $formatter, LogLevel::DEBUG); + } + + /** + * Return retry middleware. + * + * @return \Closure + */ + protected function retryMiddleware() + { + return Middleware::retry(function ( + $retries, + RequestInterface $request, + ResponseInterface $response = null + ) { + // Limit the number of retries to 2 + if ($retries < $this->app->config->get('http.max_retries', 1) && $response && $body = $response->getBody()) { + // Retry on server errors + $response = json_decode($body, true); + + if (!empty($response['errcode']) && in_array(abs($response['errcode']), [40001, 40014, 42001], true)) { + $this->accessToken->refresh(); + $this->app['logger']->debug('Retrying with refreshed access token.'); + + return true; + } + } + + return false; + }, function () { + return abs($this->app->config->get('http.retry_delay', 500)); + }); + } +} \ No newline at end of file diff --git a/src/Kernel/Config.php b/src/Kernel/Config.php index 5c7fea7..1c30703 100755 --- a/src/Kernel/Config.php +++ b/src/Kernel/Config.php @@ -2,26 +2,9 @@ namespace EasyAlipay\Kernel; -class Config +use EasyAlipay\Kernel\Support\Collection; + +class Config extends Collection { - public $config = [ - //应用ID - 'app_id' => '', - //支付宝公钥 - 'alipay_public_key' => '', - //商户私钥 - 'merchant_private_key' => '', - //网管地址 - 'gateway_url' => "https://openapi.alipay.com/gateway.do", - //异步通知地址 - 'notify_url' => "", - //同步跳转 - 'return_url' => "", - //编码格式 - 'charset' => "UTF-8", - //签名方式,默认为RSA2(RSA2048) - 'sign_type' =>"RSA2", - // ... - ]; } diff --git a/src/Kernel/Contracts/Arrayable.php b/src/Kernel/Contracts/Arrayable.php new file mode 100644 index 0000000..9818685 --- /dev/null +++ b/src/Kernel/Contracts/Arrayable.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Events; + +use EasyAlipay\Kernel\ServiceContainer; + +/** + * Class ApplicationInitialized. + * + * @author mingyoung + */ +class ApplicationInitialized +{ + /** + * @var \EasyAlipay\Kernel\ServiceContainer + */ + public $app; + + /** + * @param \EasyAlipay\Kernel\ServiceContainer $app + */ + public function __construct(ServiceContainer $app) + { + $this->app = $app; + } +} diff --git a/src/Kernel/Events/EventHandlerInterface.php b/src/Kernel/Events/EventHandlerInterface.php new file mode 100644 index 0000000..3b063e2 --- /dev/null +++ b/src/Kernel/Events/EventHandlerInterface.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Contracts; + +/** + * Interface EventHandlerInterface. + * + * @author mingyoung + */ +interface EventHandlerInterface +{ + /** + * @param mixed $payload + */ + public function handle($payload = null); +} diff --git a/src/Kernel/Events/HttpResponseCreated.php b/src/Kernel/Events/HttpResponseCreated.php new file mode 100644 index 0000000..af02ead --- /dev/null +++ b/src/Kernel/Events/HttpResponseCreated.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Events; + +use Psr\Http\Message\ResponseInterface; + +/** + * Class HttpResponseCreated. + * + * @author mingyoung + */ +class HttpResponseCreated +{ + /** + * @var \Psr\Http\Message\ResponseInterface + */ + public $response; + + /** + * @param \Psr\Http\Message\ResponseInterface $response + */ + public function __construct(ResponseInterface $response) + { + $this->response = $response; + } +} diff --git a/src/Kernel/Events/ServerGuardResponseCreated.php b/src/Kernel/Events/ServerGuardResponseCreated.php new file mode 100644 index 0000000..2b3722e --- /dev/null +++ b/src/Kernel/Events/ServerGuardResponseCreated.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Events; + +use Symfony\Component\HttpFoundation\Response; + +/** + * Class ServerGuardResponseCreated. + * + * @author mingyoung + */ +class ServerGuardResponseCreated +{ + /** + * @var \Symfony\Component\HttpFoundation\Response + */ + public $response; + + /** + * @param \Symfony\Component\HttpFoundation\Response $response + */ + public function __construct(Response $response) + { + $this->response = $response; + } +} diff --git a/src/Kernel/Exceptions/BadRequestException.php b/src/Kernel/Exceptions/BadRequestException.php new file mode 100644 index 0000000..bc2320d --- /dev/null +++ b/src/Kernel/Exceptions/BadRequestException.php @@ -0,0 +1,12 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Exceptions; + +/** + * Class InvalidArgumentException. + * + * @author overtrue + */ +class InvalidArgumentException extends Exception +{ +} diff --git a/src/Kernel/Exceptions/InvalidConfigException.php b/src/Kernel/Exceptions/InvalidConfigException.php new file mode 100644 index 0000000..95abaef --- /dev/null +++ b/src/Kernel/Exceptions/InvalidConfigException.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Exceptions; + +/** + * Class InvalidConfigException. + * + * @author overtrue + */ +class InvalidConfigException extends Exception +{ +} diff --git a/src/Kernel/Exceptions/RuntimeException.php b/src/Kernel/Exceptions/RuntimeException.php new file mode 100644 index 0000000..d2a1dfd --- /dev/null +++ b/src/Kernel/Exceptions/RuntimeException.php @@ -0,0 +1,13 @@ +app = $app; + $this->manifestPath = __DIR__.'/../extensions.php'; + } + + /** + * Get observers. + * + * @return array + */ + public function observers(): array + { + if ($this->shouldIgnore()) { + return []; + } + + $observers = []; + + foreach ($this->getManifest() as $name => $extra) { + $observers = array_merge($observers, $extra['observers'] ?? []); + } + + return array_map([$this, 'listObserver'], array_filter($observers, [$this, 'validateObserver'])); + } + + /** + * @param mixed $observer + * + * @return bool + */ + protected function isDisable($observer): bool + { + return in_array($observer, $this->app->config->get('disable_observers', [])); + } + + /** + * Get the observers should be ignore. + * + * @return bool + */ + protected function shouldIgnore(): bool + { + return !file_exists($this->manifestPath) || $this->isDisable('*'); + } + + /** + * Validate the given observer. + * + * @param mixed $observer + * + * @return bool + * + * @throws \ReflectionException + */ + protected function validateObserver($observer): bool + { + return !$this->isDisable($observer) + && (new ReflectionClass($observer))->implementsInterface(EventHandlerInterface::class) + && $this->accessible($observer); + } + + /** + * Determine whether the given observer is accessible. + * + * @param string $observer + * + * @return bool + */ + protected function accessible($observer): bool + { + if (!method_exists($observer, 'getAccessor')) { + return true; + } + + return in_array(get_class($this->app), (array) $observer::getAccessor()); + } + + /** + * @param mixed $observer + * + * @return array + */ + protected function listObserver($observer): array + { + $condition = method_exists($observer, 'onCondition') ? $observer::onCondition() : '*'; + + return [$observer, $condition]; + } + + /** + * Get the easyalipay manifest. + * + * @return array + */ + protected function getManifest(): array + { + if (!is_null($this->manifest)) { + return $this->manifest; + } + + return $this->manifest = file_exists($this->manifestPath) ? require $this->manifestPath : []; + } +} diff --git a/src/Kernel/Helpers.php b/src/Kernel/Helpers.php new file mode 100644 index 0000000..6a3bb38 --- /dev/null +++ b/src/Kernel/Helpers.php @@ -0,0 +1,82 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Http; + +use EasyAlipay\Kernel\Support\Collection; +use EasyAlipay\Kernel\Support\XML; +use GuzzleHttp\Psr7\Response as GuzzleResponse; +use Psr\Http\Message\ResponseInterface; + +/** + * Class Response. + * + * @author overtrue + */ +class Response extends GuzzleResponse +{ + /** + * @return string + */ + public function getBodyContents() + { + $this->getBody()->rewind(); + $contents = $this->getBody()->getContents(); + $this->getBody()->rewind(); + + return $contents; + } + + /** + * @param \Psr\Http\Message\ResponseInterface $response + * + * @return \EasyAlipay\Kernel\Http\Response + */ + public static function buildFromPsrResponse(ResponseInterface $response) + { + return new static( + $response->getStatusCode(), + $response->getHeaders(), + $response->getBody(), + $response->getProtocolVersion(), + $response->getReasonPhrase() + ); + } + + /** + * Build to json. + * + * @return string + */ + public function toJson() + { + return json_encode($this->toArray()); + } + + /** + * Build to array. + * + * @return array + */ + public function toArray() + { + $content = $this->removeControlCharacters($this->getBodyContents()); + + if (false !== stripos($this->getHeaderLine('Content-Type'), 'xml') || 0 === stripos($content, 'toArray()); + } + + /** + * @return object + */ + public function toObject() + { + return json_decode($this->toJson()); + } + + /** + * @return bool|string + */ + public function __toString() + { + return $this->getBodyContents(); + } + + /** + * @param string $content + * + * @return string + */ + protected function removeControlCharacters(string $content) + { + return \preg_replace('/[\x00-\x1F\x80-\x9F]/u', '', $content); + } +} diff --git a/src/Kernel/Http/StreamResponse.php b/src/Kernel/Http/StreamResponse.php new file mode 100644 index 0000000..ff39048 --- /dev/null +++ b/src/Kernel/Http/StreamResponse.php @@ -0,0 +1,86 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Http; + +use EasyAlipay\Kernel\Exceptions\InvalidArgumentException; +use EasyAlipay\Kernel\Exceptions\RuntimeException; +use EasyAlipay\Kernel\Support\File; + +/** + * Class StreamResponse. + * + * @author overtrue + */ +class StreamResponse extends Response +{ + /** + * @param string $directory + * @param string $filename + * @param bool $appendSuffix + * + * @return bool|int + * + * @throws \EasyAlipay\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyAlipay\Kernel\Exceptions\RuntimeException + */ + public function save(string $directory, string $filename = '', bool $appendSuffix = true) + { + $this->getBody()->rewind(); + + $directory = rtrim($directory, '/'); + + if (!is_dir($directory)) { + mkdir($directory, 0755, true); // @codeCoverageIgnore + } + + if (!is_writable($directory)) { + throw new InvalidArgumentException(sprintf("'%s' is not writable.", $directory)); + } + + $contents = $this->getBody()->getContents(); + + if (empty($contents) || '{' === $contents[0]) { + throw new RuntimeException('Invalid media response content.'); + } + + if (empty($filename)) { + if (preg_match('/filename="(?.*?)"/', $this->getHeaderLine('Content-Disposition'), $match)) { + $filename = $match['filename']; + } else { + $filename = md5($contents); + } + } + + if ($appendSuffix && empty(pathinfo($filename, PATHINFO_EXTENSION))) { + $filename .= File::getStreamExt($contents); + } + + file_put_contents($directory.'/'.$filename, $contents); + + return $filename; + } + + /** + * @param string $directory + * @param string $filename + * @param bool $appendSuffix + * + * @return bool|int + * + * @throws \EasyAlipay\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyAlipay\Kernel\Exceptions\RuntimeException + */ + public function saveAs(string $directory, string $filename, bool $appendSuffix = true) + { + return $this->save($directory, $filename, $appendSuffix); + } +} diff --git a/src/Kernel/Log/LogManager.php b/src/Kernel/Log/LogManager.php new file mode 100644 index 0000000..4948a04 --- /dev/null +++ b/src/Kernel/Log/LogManager.php @@ -0,0 +1,608 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Log; + +use EasyAlipay\Kernel\ServiceContainer; +use InvalidArgumentException; +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\ErrorLogHandler; +use Monolog\Handler\FormattableHandlerInterface; +use Monolog\Handler\HandlerInterface; +use Monolog\Handler\RotatingFileHandler; +use Monolog\Handler\SlackWebhookHandler; +use Monolog\Handler\StreamHandler; +use Monolog\Handler\SyslogHandler; +use Monolog\Handler\WhatFailureGroupHandler; +use Monolog\Logger as Monolog; +use Psr\Log\LoggerInterface; + +/** + * Class LogManager. + * + * @author overtrue + */ +class LogManager implements LoggerInterface +{ + /** + * @var \EasyAlipay\Kernel\ServiceContainer + */ + protected $app; + + /** + * The array of resolved channels. + * + * @var array + */ + protected $channels = []; + + /** + * The registered custom driver creators. + * + * @var array + */ + protected $customCreators = []; + + /** + * The Log levels. + * + * @var array + */ + protected $levels = [ + 'debug' => Monolog::DEBUG, + 'info' => Monolog::INFO, + 'notice' => Monolog::NOTICE, + 'warning' => Monolog::WARNING, + 'error' => Monolog::ERROR, + 'critical' => Monolog::CRITICAL, + 'alert' => Monolog::ALERT, + 'emergency' => Monolog::EMERGENCY, + ]; + + /** + * LogManager constructor. + * + * @param \EasyAlipay\Kernel\ServiceContainer $app + */ + public function __construct(ServiceContainer $app) + { + $this->app = $app; + } + + /** + * Create a new, on-demand aggregate logger instance. + * + * @param array $channels + * @param string|null $channel + * + * @return \Psr\Log\LoggerInterface + * + * @throws \Exception + */ + public function stack(array $channels, $channel = null) + { + return $this->createStackDriver(compact('channels', 'channel')); + } + + /** + * Get a log channel instance. + * + * @param string|null $channel + * + * @return mixed + * + * @throws \Exception + */ + public function channel($channel = null) + { + return $this->driver($channel); + } + + /** + * Get a log driver instance. + * + * @param string|null $driver + * + * @return mixed + * + * @throws \Exception + */ + public function driver($driver = null) + { + return $this->get($driver ?? $this->getDefaultDriver()); + } + + /** + * Attempt to get the log from the local cache. + * + * @param string $name + * + * @return \Psr\Log\LoggerInterface + * + * @throws \Exception + */ + protected function get($name) + { + try { + return $this->channels[$name] ?? ($this->channels[$name] = $this->resolve($name)); + } catch (\Throwable $e) { + $logger = $this->createEmergencyLogger(); + + $logger->emergency('Unable to create configured logger. Using emergency logger.', [ + 'exception' => $e, + ]); + + return $logger; + } + } + + /** + * Resolve the given log instance by name. + * + * @param string $name + * + * @return \Psr\Log\LoggerInterface + * + * @throws InvalidArgumentException + */ + protected function resolve($name) + { + $config = $this->app['config']->get(\sprintf('log.channels.%s', $name)); + + if (is_null($config)) { + throw new InvalidArgumentException(\sprintf('Log [%s] is not defined.', $name)); + } + + if (isset($this->customCreators[$config['driver']])) { + return $this->callCustomCreator($config); + } + + $driverMethod = 'create'.ucfirst($config['driver']).'Driver'; + + if (method_exists($this, $driverMethod)) { + return $this->{$driverMethod}($config); + } + + throw new InvalidArgumentException(\sprintf('Driver [%s] is not supported.', $config['driver'])); + } + + /** + * Create an emergency log handler to avoid white screens of death. + * + * @return \Monolog\Logger + * + * @throws \Exception + */ + protected function createEmergencyLogger() + { + return new Monolog('EasyAlipay', $this->prepareHandlers([new StreamHandler( + \sys_get_temp_dir().'/easyalipay/easyalipay.log', + $this->level(['level' => 'debug']) + )])); + } + + /** + * Call a custom driver creator. + * + * @param array $config + * + * @return mixed + */ + protected function callCustomCreator(array $config) + { + return $this->customCreators[$config['driver']]($this->app, $config); + } + + /** + * Create an aggregate log driver instance. + * + * @param array $config + * + * @return \Monolog\Logger + * + * @throws \Exception + */ + protected function createStackDriver(array $config) + { + $handlers = []; + + foreach ($config['channels'] ?? [] as $channel) { + $handlers = \array_merge($handlers, $this->channel($channel)->getHandlers()); + } + + if ($config['ignore_exceptions'] ?? false) { + $handlers = [new WhatFailureGroupHandler($handlers)]; + } + + return new Monolog($this->parseChannel($config), $handlers); + } + + /** + * Create an instance of the single file log driver. + * + * @param array $config + * + * @return \Psr\Log\LoggerInterface + * + * @throws \Exception + */ + protected function createSingleDriver(array $config) + { + return new Monolog($this->parseChannel($config), [ + $this->prepareHandler(new StreamHandler( + $config['path'], + $this->level($config), + $config['bubble'] ?? true, + $config['permission'] ?? null, + $config['locking'] ?? false + ), $config), + ]); + } + + /** + * Create an instance of the daily file log driver. + * + * @param array $config + * + * @return \Psr\Log\LoggerInterface + */ + protected function createDailyDriver(array $config) + { + return new Monolog($this->parseChannel($config), [ + $this->prepareHandler(new RotatingFileHandler( + $config['path'], + $config['days'] ?? 7, + $this->level($config), + $config['bubble'] ?? true, + $config['permission'] ?? null, + $config['locking'] ?? false + ), $config), + ]); + } + + /** + * Create an instance of the Slack log driver. + * + * @param array $config + * + * @return \Psr\Log\LoggerInterface + */ + protected function createSlackDriver(array $config) + { + return new Monolog($this->parseChannel($config), [ + $this->prepareHandler(new SlackWebhookHandler( + $config['url'], + $config['channel'] ?? null, + $config['username'] ?? 'EasyAliPay', + $config['attachment'] ?? true, + $config['emoji'] ?? ':boom:', + $config['short'] ?? false, + $config['context'] ?? true, + $this->level($config), + $config['bubble'] ?? true, + $config['exclude_fields'] ?? [] + ), $config), + ]); + } + + /** + * Create an instance of the syslog log driver. + * + * @param array $config + * + * @return \Psr\Log\LoggerInterface + */ + protected function createSyslogDriver(array $config) + { + return new Monolog($this->parseChannel($config), [ + $this->prepareHandler(new SyslogHandler( + 'EasyAlipay', + $config['facility'] ?? LOG_USER, + $this->level($config) + ), $config), + ]); + } + + /** + * Create an instance of the "error log" log driver. + * + * @param array $config + * + * @return \Psr\Log\LoggerInterface + */ + protected function createErrorlogDriver(array $config) + { + return new Monolog($this->parseChannel($config), [ + $this->prepareHandler( + new ErrorLogHandler( + $config['type'] ?? ErrorLogHandler::OPERATING_SYSTEM, + $this->level($config) + ) + ), + ]); + } + + /** + * Prepare the handlers for usage by Monolog. + * + * @param array $handlers + * + * @return array + */ + protected function prepareHandlers(array $handlers) + { + foreach ($handlers as $key => $handler) { + $handlers[$key] = $this->prepareHandler($handler); + } + + return $handlers; + } + + /** + * Prepare the handler for usage by Monolog. + * + * @param \Monolog\Handler\HandlerInterface $handler + * + * @return \Monolog\Handler\HandlerInterface + */ + protected function prepareHandler(HandlerInterface $handler, array $config = []) + { + if (!isset($config['formatter'])) { + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($this->formatter()); + } + } + + return $handler; + } + + /** + * Get a Monolog formatter instance. + * + * @return \Monolog\Formatter\FormatterInterface + */ + protected function formatter() + { + $formatter = new LineFormatter(null, null, true, true); + $formatter->includeStacktraces(); + + return $formatter; + } + + /** + * Extract the log channel from the given configuration. + * + * @param array $config + * + * @return string + */ + protected function parseChannel(array $config) + { + return $config['name'] ?? 'EasyAlipay'; + } + + /** + * Parse the string level into a Monolog constant. + * + * @param array $config + * + * @return int + * + * @throws InvalidArgumentException + */ + protected function level(array $config) + { + $level = $config['level'] ?? 'debug'; + + if (isset($this->levels[$level])) { + return $this->levels[$level]; + } + + throw new InvalidArgumentException('Invalid log level.'); + } + + /** + * Get the default log driver name. + * + * @return string + */ + public function getDefaultDriver() + { + return $this->app['config']['log.default']; + } + + /** + * Set the default log driver name. + * + * @param string $name + */ + public function setDefaultDriver($name) + { + $this->app['config']['log.default'] = $name; + } + + /** + * Register a custom driver creator Closure. + * + * @param string $driver + * @param \Closure $callback + * + * @return $this + */ + public function extend($driver, \Closure $callback) + { + $this->customCreators[$driver] = $callback->bindTo($this, $this); + + return $this; + } + + /** + * System is unusable. + * + * @param string $message + * @param array $context + * + * @return mixed + * + * @throws \Exception + */ + public function emergency($message, array $context = []) + { + return $this->driver()->emergency($message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return mixed + * + * @throws \Exception + */ + public function alert($message, array $context = []) + { + return $this->driver()->alert($message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return mixed + * + * @throws \Exception + */ + public function critical($message, array $context = []) + { + return $this->driver()->critical($message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return mixed + * + * @throws \Exception + */ + public function error($message, array $context = []) + { + return $this->driver()->error($message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return mixed + * + * @throws \Exception + */ + public function warning($message, array $context = []) + { + return $this->driver()->warning($message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return mixed + * + * @throws \Exception + */ + public function notice($message, array $context = []) + { + return $this->driver()->notice($message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return mixed + * + * @throws \Exception + */ + public function info($message, array $context = []) + { + return $this->driver()->info($message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return mixed + * + * @throws \Exception + */ + public function debug($message, array $context = []) + { + return $this->driver()->debug($message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return mixed + * + * @throws \Exception + */ + public function log($level, $message, array $context = []) + { + return $this->driver()->log($level, $message, $context); + } + + /** + * Dynamically call the default driver instance. + * + * @param string $method + * @param array $parameters + * + * @return mixed + * + * @throws \Exception + */ + public function __call($method, $parameters) + { + return $this->driver()->$method(...$parameters); + } +} diff --git a/src/Kernel/Providers/ConfigServiceProvider.php b/src/Kernel/Providers/ConfigServiceProvider.php index 9299a18..f06098c 100755 --- a/src/Kernel/Providers/ConfigServiceProvider.php +++ b/src/Kernel/Providers/ConfigServiceProvider.php @@ -20,7 +20,7 @@ class ConfigServiceProvider implements ServiceProviderInterface public function register(Container $pimple) { $pimple['config'] = function ($app) { - return $app->getConfig(); + return new Config($app->getConfig()); }; } } diff --git a/src/Kernel/Providers/EventDispatcherServiceProvider.php b/src/Kernel/Providers/EventDispatcherServiceProvider.php new file mode 100644 index 0000000..21e0765 --- /dev/null +++ b/src/Kernel/Providers/EventDispatcherServiceProvider.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Providers; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; + +/** + * Class EventDispatcherServiceProvider. + * + * @author mingyoung + */ +class EventDispatcherServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + * + * @param Container $pimple A container instance + */ + public function register(Container $pimple) + { + $pimple['events'] = function ($app) { + $dispatcher = new EventDispatcher(); + + foreach ($app->config->get('events.listen', []) as $event => $listeners) { + foreach ($listeners as $listener) { + $dispatcher->addListener($event, $listener); + } + } + + return $dispatcher; + }; + } +} diff --git a/src/Kernel/Providers/ExtensionServiceProvider.php b/src/Kernel/Providers/ExtensionServiceProvider.php new file mode 100644 index 0000000..53bc65b --- /dev/null +++ b/src/Kernel/Providers/ExtensionServiceProvider.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Providers; + +use EasyAlipay\Kernel\Extension; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ExtensionServiceProvider. + * + * @author overtrue + */ +class ExtensionServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + * + * @param Container $pimple A container instance + */ + public function register(Container $pimple) + { + $pimple['extension'] = function ($app) { + return new Extension($app); + }; + } +} diff --git a/src/Kernel/Providers/HttpClientServiceProvider.php b/src/Kernel/Providers/HttpClientServiceProvider.php new file mode 100644 index 0000000..35b9363 --- /dev/null +++ b/src/Kernel/Providers/HttpClientServiceProvider.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Providers; + +use GuzzleHttp\Client; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class HttpClientServiceProvider. + * + * @author overtrue + */ +class HttpClientServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + * + * @param Container $pimple A container instance + */ + public function register(Container $pimple) + { + $pimple['http_client'] = function ($app) { + return new Client($app['config']->get('http', [])); + }; + } +} diff --git a/src/Kernel/Providers/LogServiceProvider.php b/src/Kernel/Providers/LogServiceProvider.php new file mode 100644 index 0000000..93d335a --- /dev/null +++ b/src/Kernel/Providers/LogServiceProvider.php @@ -0,0 +1,79 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Providers; + +use EasyAlipay\Kernel\Log\LogManager; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class LoggingServiceProvider. + * + * @author overtrue + */ +class LogServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + * + * @param Container $pimple A container instance + */ + public function register(Container $pimple) + { + $pimple['logger'] = $pimple['log'] = function ($app) { + $config = $this->formatLogConfig($app); + + if (!empty($config)) { + $app->rebind('config', $app['config']->merge($config)); + } + + return new LogManager($app); + }; + } + + public function formatLogConfig($app) + { + if (!empty($app['config']->get('log.channels'))) { + return $app['config']->get('log'); + } + + if (empty($app['config']->get('log'))) { + return [ + 'log' => [ + 'default' => 'errorlog', + 'channels' => [ + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => 'debug', + ], + ], + ], + ]; + } + + return [ + 'log' => [ + 'default' => 'single', + 'channels' => [ + 'single' => [ + 'driver' => 'single', + 'path' => $app['config']->get('log.file') ?: \sys_get_temp_dir().'/logs/easyalipay.log', + 'level' => $app['config']->get('log.level', 'debug'), + ], + ], + ], + ]; + } +} diff --git a/src/Kernel/Providers/RequestServiceProvider.php b/src/Kernel/Providers/RequestServiceProvider.php new file mode 100644 index 0000000..6cea093 --- /dev/null +++ b/src/Kernel/Providers/RequestServiceProvider.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Providers; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * Class RequestServiceProvider. + * + * @author overtrue + */ +class RequestServiceProvider implements ServiceProviderInterface +{ + /** + * Registers services on the given container. + * + * This method should only be used to configure services and parameters. + * It should not get services. + * + * @param Container $pimple A container instance + */ + public function register(Container $pimple) + { + $pimple['request'] = function () { + return Request::createFromGlobals(); + }; + } +} diff --git a/src/Kernel/ServiceContainer.php b/src/Kernel/ServiceContainer.php index 761a6f5..b7317c9 100755 --- a/src/Kernel/ServiceContainer.php +++ b/src/Kernel/ServiceContainer.php @@ -2,14 +2,22 @@ namespace EasyAlipay\Kernel; -use EasyAlipay\Kernel\Config; use EasyAlipay\Kernel\Providers\ConfigServiceProvider; +use EasyAlipay\Kernel\Providers\LogServiceProvider; +use EasyAlipay\Kernel\Providers\EventDispatcherServiceProvider; +use EasyAlipay\Kernel\Providers\ExtensionServiceProvider; +use EasyAlipay\Kernel\Providers\HttpClientServiceProvider; +use EasyAlipay\Kernel\Providers\RequestServiceProvider; use Pimple\Container; /** * Class ServiceContainer * - * @property \EasyAlipay\Kernel\Config $config + * @property \EasyAlipay\Kernel\Config $config + * @property \Symfony\Component\HttpFoundation\Request $request + * @property \GuzzleHttp\Client $http_client + * @property \Monolog\Logger $logger + * @property \Symfony\Component\EventDispatcher\EventDispatcher $events */ class ServiceContainer extends Container { @@ -31,20 +39,26 @@ class ServiceContainer extends Container /** * Constructor. * - * @param array $config + * @param array $config */ public function __construct(array $config = []) { $this->registerProviders($this->getProviders()); parent::__construct(); - $this->userConfig = $config; - $config = new config(); - $this->defaultConfig = $config->config; + $this->userConfig = $config; + $this->events->dispatch(new Events\ApplicationInitialized($this)); } public function getConfig() { - return array_replace_recursive($this->defaultConfig, $this->userConfig); + $base = [ + // http://docs.guzzlephp.org/en/stable/request-options.html + 'http' => [ + 'timeout' => 30.0, + 'base_uri' => 'https://openapi.alipay.com/gateway.do', + ], + ]; + return array_replace_recursive($base, $this->defaultConfig, $this->userConfig); } @@ -57,9 +71,47 @@ class ServiceContainer extends Container { return array_merge([ ConfigServiceProvider::class, + LogServiceProvider::class, + RequestServiceProvider::class, + HttpClientServiceProvider::class, + ExtensionServiceProvider::class, + EventDispatcherServiceProvider::class, ], $this->providers); } + /** + * @param string $id + * @param mixed $value + */ + public function rebind($id, $value) + { + $this->offsetUnset($id); + $this->offsetSet($id, $value); + } + + /** + * Magic get access. + * + * @param string $id + * + * @return mixed + */ + public function __get($id) + { + return $this->offsetGet($id); + } + + /** + * Magic set access. + * + * @param string $id + * @param mixed $value + */ + public function __set($id, $value) + { + $this->offsetSet($id, $value); + } + /** * @param array $providers */ diff --git a/src/Kernel/Support/Arr.php b/src/Kernel/Support/Arr.php new file mode 100644 index 0000000..971c5f0 --- /dev/null +++ b/src/Kernel/Support/Arr.php @@ -0,0 +1,466 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Support; + +/** + * Array helper from Illuminate\Support\Arr. + */ +class Arr +{ + /** + * Add an element to an array using "dot" notation if it doesn't exist. + * + * @param array $array + * @param string $key + * @param mixed $value + * + * @return array + */ + public static function add(array $array, $key, $value) + { + if (is_null(static::get($array, $key))) { + static::set($array, $key, $value); + } + + return $array; + } + + /** + * Cross join the given arrays, returning all possible permutations. + * + * @param array ...$arrays + * + * @return array + */ + public static function crossJoin(...$arrays) + { + $results = [[]]; + + foreach ($arrays as $index => $array) { + $append = []; + + foreach ($results as $product) { + foreach ($array as $item) { + $product[$index] = $item; + + $append[] = $product; + } + } + + $results = $append; + } + + return $results; + } + + /** + * Divide an array into two arrays. One with keys and the other with values. + * + * @param array $array + * + * @return array + */ + public static function divide(array $array) + { + return [array_keys($array), array_values($array)]; + } + + /** + * Flatten a multi-dimensional associative array with dots. + * + * @param array $array + * @param string $prepend + * + * @return array + */ + public static function dot(array $array, $prepend = '') + { + $results = []; + + foreach ($array as $key => $value) { + if (is_array($value) && !empty($value)) { + $results = array_merge($results, static::dot($value, $prepend.$key.'.')); + } else { + $results[$prepend.$key] = $value; + } + } + + return $results; + } + + /** + * Get all of the given array except for a specified array of items. + * + * @param array $array + * @param array|string $keys + * + * @return array + */ + public static function except(array $array, $keys) + { + static::forget($array, $keys); + + return $array; + } + + /** + * Determine if the given key exists in the provided array. + * + * @param array $array + * @param string|int $key + * + * @return bool + */ + public static function exists(array $array, $key) + { + return array_key_exists($key, $array); + } + + /** + * Return the first element in an array passing a given truth test. + * + * @param array $array + * @param callable|null $callback + * @param mixed $default + * + * @return mixed + */ + public static function first(array $array, callable $callback = null, $default = null) + { + if (is_null($callback)) { + if (empty($array)) { + return $default; + } + + foreach ($array as $item) { + return $item; + } + } + + foreach ($array as $key => $value) { + if (call_user_func($callback, $value, $key)) { + return $value; + } + } + + return $default; + } + + /** + * Return the last element in an array passing a given truth test. + * + * @param array $array + * @param callable|null $callback + * @param mixed $default + * + * @return mixed + */ + public static function last(array $array, callable $callback = null, $default = null) + { + if (is_null($callback)) { + return empty($array) ? $default : end($array); + } + + return static::first(array_reverse($array, true), $callback, $default); + } + + /** + * Flatten a multi-dimensional array into a single level. + * + * @param array $array + * @param int $depth + * + * @return array + */ + public static function flatten(array $array, $depth = INF) + { + return array_reduce($array, function ($result, $item) use ($depth) { + $item = $item instanceof Collection ? $item->all() : $item; + + if (!is_array($item)) { + return array_merge($result, [$item]); + } elseif (1 === $depth) { + return array_merge($result, array_values($item)); + } + + return array_merge($result, static::flatten($item, $depth - 1)); + }, []); + } + + /** + * Remove one or many array items from a given array using "dot" notation. + * + * @param array $array + * @param array|string $keys + */ + public static function forget(array &$array, $keys) + { + $original = &$array; + + $keys = (array) $keys; + + if (0 === count($keys)) { + return; + } + + foreach ($keys as $key) { + // if the exact key exists in the top-level, remove it + if (static::exists($array, $key)) { + unset($array[$key]); + + continue; + } + + $parts = explode('.', $key); + + // clean up before each pass + $array = &$original; + + while (count($parts) > 1) { + $part = array_shift($parts); + + if (isset($array[$part]) && is_array($array[$part])) { + $array = &$array[$part]; + } else { + continue 2; + } + } + + unset($array[array_shift($parts)]); + } + } + + /** + * Get an item from an array using "dot" notation. + * + * @param array $array + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public static function get(array $array, $key, $default = null) + { + if (is_null($key)) { + return $array; + } + + if (static::exists($array, $key)) { + return $array[$key]; + } + + foreach (explode('.', $key) as $segment) { + if (static::exists($array, $segment)) { + $array = $array[$segment]; + } else { + return $default; + } + } + + return $array; + } + + /** + * Check if an item or items exist in an array using "dot" notation. + * + * @param array $array + * @param string|array $keys + * + * @return bool + */ + public static function has(array $array, $keys) + { + if (is_null($keys)) { + return false; + } + + $keys = (array) $keys; + + if (empty($array)) { + return false; + } + + if ($keys === []) { + return false; + } + + foreach ($keys as $key) { + $subKeyArray = $array; + + if (static::exists($array, $key)) { + continue; + } + + foreach (explode('.', $key) as $segment) { + if (static::exists($subKeyArray, $segment)) { + $subKeyArray = $subKeyArray[$segment]; + } else { + return false; + } + } + } + + return true; + } + + /** + * Determines if an array is associative. + * + * An array is "associative" if it doesn't have sequential numerical keys beginning with zero. + * + * @param array $array + * + * @return bool + */ + public static function isAssoc(array $array) + { + $keys = array_keys($array); + + return array_keys($keys) !== $keys; + } + + /** + * Get a subset of the items from the given array. + * + * @param array $array + * @param array|string $keys + * + * @return array + */ + public static function only(array $array, $keys) + { + return array_intersect_key($array, array_flip((array) $keys)); + } + + /** + * Push an item onto the beginning of an array. + * + * @param array $array + * @param mixed $value + * @param mixed $key + * + * @return array + */ + public static function prepend(array $array, $value, $key = null) + { + if (is_null($key)) { + array_unshift($array, $value); + } else { + $array = [$key => $value] + $array; + } + + return $array; + } + + /** + * Get a value from the array, and remove it. + * + * @param array $array + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public static function pull(array &$array, $key, $default = null) + { + $value = static::get($array, $key, $default); + + static::forget($array, $key); + + return $value; + } + + /** + * Get a 1 value from an array. + * + * @param array $array + * @param int|null $amount + * + * @return mixed + * + * @throws \InvalidArgumentException + */ + public static function random(array $array, int $amount = null) + { + if (is_null($amount)) { + return $array[array_rand($array)]; + } + + $keys = array_rand($array, $amount); + + $results = []; + + foreach ((array) $keys as $key) { + $results[] = $array[$key]; + } + + return $results; + } + + /** + * Set an array item to a given value using "dot" notation. + * + * If no key is given to the method, the entire array will be replaced. + * + * @param array $array + * @param string $key + * @param mixed $value + * + * @return array + */ + public static function set(array &$array, string $key, $value) + { + $keys = explode('.', $key); + + while (count($keys) > 1) { + $key = array_shift($keys); + + // If the key doesn't exist at this depth, we will just create an empty array + // to hold the next value, allowing us to create the arrays to hold final + // values at the correct depth. Then we'll keep digging into the array. + if (!isset($array[$key]) || !is_array($array[$key])) { + $array[$key] = []; + } + + $array = &$array[$key]; + } + + $array[array_shift($keys)] = $value; + + return $array; + } + + /** + * Filter the array using the given callback. + * + * @param array $array + * @param callable $callback + * + * @return array + */ + public static function where(array $array, callable $callback) + { + return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH); + } + + /** + * If the given value is not an array, wrap it in one. + * + * @param mixed $value + * + * @return array + */ + public static function wrap($value) + { + return !is_array($value) ? [$value] : $value; + } +} diff --git a/src/Kernel/Support/Collection.php b/src/Kernel/Support/Collection.php new file mode 100644 index 0000000..91810cf --- /dev/null +++ b/src/Kernel/Support/Collection.php @@ -0,0 +1,420 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Support; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use JsonSerializable; +use Serializable; + +/** + * Class Collection. + */ +class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Serializable +{ + /** + * The collection data. + * + * @var array + */ + protected $items = []; + + /** + * set data. + * + * @param array $items + */ + public function __construct(array $items = []) + { + foreach ($items as $key => $value) { + $this->set($key, $value); + } + } + + /** + * Return all items. + * + * @return array + */ + public function all() + { + return $this->items; + } + + /** + * Return specific items. + * + * @param array $keys + * + * @return \EasyAlipay\Kernel\Support\Collection + */ + public function only(array $keys) + { + $return = []; + + foreach ($keys as $key) { + $value = $this->get($key); + + if (!is_null($value)) { + $return[$key] = $value; + } + } + + return new static($return); + } + + /** + * Get all items except for those with the specified keys. + * + * @param mixed $keys + * + * @return static + */ + public function except($keys) + { + $keys = is_array($keys) ? $keys : func_get_args(); + + return new static(Arr::except($this->items, $keys)); + } + + /** + * Merge data. + * + * @param Collection|array $items + * + * @return \EasyAlipay\Kernel\Support\Collection + */ + public function merge($items) + { + $clone = new static($this->all()); + + foreach ($items as $key => $value) { + $clone->set($key, $value); + } + + return $clone; + } + + /** + * To determine Whether the specified element exists. + * + * @param string $key + * + * @return bool + */ + public function has($key) + { + return !is_null(Arr::get($this->items, $key)); + } + + /** + * Retrieve the first item. + * + * @return mixed + */ + public function first() + { + return reset($this->items); + } + + /** + * Retrieve the last item. + * + * @return bool + */ + public function last() + { + $end = end($this->items); + + reset($this->items); + + return $end; + } + + /** + * add the item value. + * + * @param string $key + * @param mixed $value + */ + public function add($key, $value) + { + Arr::set($this->items, $key, $value); + } + + /** + * Set the item value. + * + * @param string $key + * @param mixed $value + */ + public function set($key, $value) + { + Arr::set($this->items, $key, $value); + } + + /** + * Retrieve item from Collection. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function get($key, $default = null) + { + return Arr::get($this->items, $key, $default); + } + + /** + * Remove item form Collection. + * + * @param string $key + */ + public function forget($key) + { + Arr::forget($this->items, $key); + } + + /** + * Build to array. + * + * @return array + */ + public function toArray() + { + return $this->all(); + } + + /** + * Build to json. + * + * @param int $option + * + * @return string + */ + public function toJson($option = JSON_UNESCAPED_UNICODE) + { + return json_encode($this->all(), $option); + } + + /** + * To string. + * + * @return string + */ + public function __toString() + { + return $this->toJson(); + } + + /** + * (PHP 5 >= 5.4.0)
+ * Specify data which should be serialized to JSON. + * + * @see http://php.net/manual/en/jsonserializable.jsonserialize.php + * + * @return mixed data which can be serialized by json_encode, + * which is a value of any type other than a resource + */ + public function jsonSerialize() + { + return $this->items; + } + + /** + * (PHP 5 >= 5.1.0)
+ * String representation of object. + * + * @see http://php.net/manual/en/serializable.serialize.php + * + * @return string the string representation of the object or null + */ + public function serialize() + { + return serialize($this->items); + } + + /** + * (PHP 5 >= 5.0.0)
+ * Retrieve an external iterator. + * + * @see http://php.net/manual/en/iteratoraggregate.getiterator.php + * + * @return \ArrayIterator An instance of an object implementing Iterator or + * Traversable + */ + public function getIterator() + { + return new ArrayIterator($this->items); + } + + /** + * (PHP 5 >= 5.1.0)
+ * Count elements of an object. + * + * @see http://php.net/manual/en/countable.count.php + * + * @return int the custom count as an integer. + *

+ *

+ * The return value is cast to an integer + */ + public function count() + { + return count($this->items); + } + + /** + * (PHP 5 >= 5.1.0)
+ * Constructs the object. + * + * @see http://php.net/manual/en/serializable.unserialize.php + * + * @param string $serialized

+ * The string representation of the object. + *

+ * + * @return mixed|void + */ + public function unserialize($serialized) + { + return $this->items = unserialize($serialized); + } + + /** + * Get a data by key. + * + * @param string $key + * + * @return mixed + */ + public function __get($key) + { + return $this->get($key); + } + + /** + * Assigns a value to the specified data. + * + * @param string $key + * @param mixed $value + */ + public function __set($key, $value) + { + $this->set($key, $value); + } + + /** + * Whether or not an data exists by key. + * + * @param string $key + * + * @return bool + */ + public function __isset($key) + { + return $this->has($key); + } + + /** + * Unset an data by key. + * + * @param string $key + */ + public function __unset($key) + { + $this->forget($key); + } + + /** + * var_export. + * + * @return array + */ + public function __set_state() + { + return $this->all(); + } + + /** + * (PHP 5 >= 5.0.0)
+ * Whether a offset exists. + * + * @see http://php.net/manual/en/arrayaccess.offsetexists.php + * + * @param mixed $offset

+ * An offset to check for. + *

+ * + * @return bool true on success or false on failure. + * The return value will be casted to boolean if non-boolean was returned + */ + public function offsetExists($offset) + { + return $this->has($offset); + } + + /** + * (PHP 5 >= 5.0.0)
+ * Offset to unset. + * + * @see http://php.net/manual/en/arrayaccess.offsetunset.php + * + * @param mixed $offset

+ * The offset to unset. + *

+ */ + public function offsetUnset($offset) + { + if ($this->offsetExists($offset)) { + $this->forget($offset); + } + } + + /** + * (PHP 5 >= 5.0.0)
+ * Offset to retrieve. + * + * @see http://php.net/manual/en/arrayaccess.offsetget.php + * + * @param mixed $offset

+ * The offset to retrieve. + *

+ * + * @return mixed Can return all value types + */ + public function offsetGet($offset) + { + return $this->offsetExists($offset) ? $this->get($offset) : null; + } + + /** + * (PHP 5 >= 5.0.0)
+ * Offset to set. + * + * @see http://php.net/manual/en/arrayaccess.offsetset.php + * + * @param mixed $offset

+ * The offset to assign the value to. + *

+ * @param mixed $value

+ * The value to set. + *

+ */ + public function offsetSet($offset, $value) + { + $this->set($offset, $value); + } +} diff --git a/src/Kernel/Support/File.php b/src/Kernel/Support/File.php new file mode 100644 index 0000000..d1c4e2f --- /dev/null +++ b/src/Kernel/Support/File.php @@ -0,0 +1,135 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Support; + +use finfo; + +/** + * Class File. + */ +class File +{ + /** + * MIME mapping. + * + * @var array + */ + protected static $extensionMap = [ + 'audio/wav' => '.wav', + 'audio/x-ms-wma' => '.wma', + 'video/x-ms-wmv' => '.wmv', + 'video/mp4' => '.mp4', + 'audio/mpeg' => '.mp3', + 'audio/amr' => '.amr', + 'application/vnd.rn-realmedia' => '.rm', + 'audio/mid' => '.mid', + 'image/bmp' => '.bmp', + 'image/gif' => '.gif', + 'image/png' => '.png', + 'image/tiff' => '.tiff', + 'image/jpeg' => '.jpg', + 'application/pdf' => '.pdf', + + // 列举更多的文件 mime, 企业号是支持的,公众平台这边之后万一也更新了呢 + 'application/msword' => '.doc', + + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => '.docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => '.dotx', + 'application/vnd.ms-word.document.macroEnabled.12' => '.docm', + 'application/vnd.ms-word.template.macroEnabled.12' => '.dotm', + + 'application/vnd.ms-excel' => '.xls', + + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => '.xlsx', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => '.xltx', + 'application/vnd.ms-excel.sheet.macroEnabled.12' => '.xlsm', + 'application/vnd.ms-excel.template.macroEnabled.12' => '.xltm', + 'application/vnd.ms-excel.addin.macroEnabled.12' => '.xlam', + 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => '.xlsb', + + 'application/vnd.ms-powerpoint' => '.ppt', + + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => '.pptx', + 'application/vnd.openxmlformats-officedocument.presentationml.template' => '.potx', + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => '.ppsx', + 'application/vnd.ms-powerpoint.addin.macroEnabled.12' => '.ppam', + ]; + + /** + * File header signatures. + * + * @var array + */ + protected static $signatures = [ + 'ffd8ff' => '.jpg', + '424d' => '.bmp', + '47494638' => '.gif', + '2f55736572732f6f7665' => '.png', + '89504e47' => '.png', + '494433' => '.mp3', + 'fffb' => '.mp3', + 'fff3' => '.mp3', + '3026b2758e66cf11' => '.wma', + '52494646' => '.wav', + '57415645' => '.wav', + '41564920' => '.avi', + '000001ba' => '.mpg', + '000001b3' => '.mpg', + '2321414d52' => '.amr', + '25504446' => '.pdf', + ]; + + /** + * Return steam extension. + * + * @param string $stream + * + * @return string|false + */ + public static function getStreamExt($stream) + { + $ext = self::getExtBySignature($stream); + + try { + if (empty($ext) && is_readable($stream)) { + $stream = file_get_contents($stream); + } + } catch (\Exception $e) { + } + + $fileInfo = new finfo(FILEINFO_MIME); + + $mime = strstr($fileInfo->buffer($stream), ';', true); + + return isset(self::$extensionMap[$mime]) ? self::$extensionMap[$mime] : $ext; + } + + /** + * Get file extension by file header signature. + * + * @param string $stream + * + * @return string + */ + public static function getExtBySignature($stream) + { + $prefix = strval(bin2hex(mb_strcut($stream, 0, 10))); + + foreach (self::$signatures as $signature => $extension) { + if (0 === strpos($prefix, strval($signature))) { + return $extension; + } + } + + return ''; + } +} diff --git a/src/Kernel/Support/XML.php b/src/Kernel/Support/XML.php new file mode 100644 index 0000000..9732ba5 --- /dev/null +++ b/src/Kernel/Support/XML.php @@ -0,0 +1,167 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace EasyAlipay\Kernel\Support; + +use SimpleXMLElement; + +/** + * Class XML. + */ +class XML +{ + /** + * XML to array. + * + * @param string $xml XML string + * + * @return array + */ + public static function parse($xml) + { + $backup = libxml_disable_entity_loader(true); + + $result = self::normalize(simplexml_load_string(self::sanitize($xml), 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_NOCDATA | LIBXML_NOBLANKS)); + + libxml_disable_entity_loader($backup); + + return $result; + } + + /** + * XML encode. + * + * @param mixed $data + * @param string $root + * @param string $item + * @param string $attr + * @param string $id + * + * @return string + */ + public static function build( + $data, + $root = 'xml', + $item = 'item', + $attr = '', + $id = 'id' + ) { + if (is_array($attr)) { + $_attr = []; + + foreach ($attr as $key => $value) { + $_attr[] = "{$key}=\"{$value}\""; + } + + $attr = implode(' ', $_attr); + } + + $attr = trim($attr); + $attr = empty($attr) ? '' : " {$attr}"; + $xml = "<{$root}{$attr}>"; + $xml .= self::data2Xml($data, $item, $id); + $xml .= ""; + + return $xml; + } + + /** + * Build CDATA. + * + * @param string $string + * + * @return string + */ + public static function cdata($string) + { + return sprintf('', $string); + } + + /** + * Object to array. + * + * + * @param SimpleXMLElement $obj + * + * @return array + */ + protected static function normalize($obj) + { + $result = null; + + if (is_object($obj)) { + $obj = (array) $obj; + } + + if (is_array($obj)) { + foreach ($obj as $key => $value) { + $res = self::normalize($value); + if (('@attributes' === $key) && ($key)) { + $result = $res; // @codeCoverageIgnore + } else { + $result[$key] = $res; + } + } + } else { + $result = $obj; + } + + return $result; + } + + /** + * Array to XML. + * + * @param array $data + * @param string $item + * @param string $id + * + * @return string + */ + protected static function data2Xml($data, $item = 'item', $id = 'id') + { + $xml = $attr = ''; + + foreach ($data as $key => $val) { + if (is_numeric($key)) { + $id && $attr = " {$id}=\"{$key}\""; + $key = $item; + } + + $xml .= "<{$key}{$attr}>"; + + if ((is_array($val) || is_object($val))) { + $xml .= self::data2Xml((array) $val, $item, $id); + } else { + $xml .= is_numeric($val) ? $val : self::cdata($val); + } + + $xml .= ""; + } + + return $xml; + } + + /** + * Delete invalid characters in XML. + * + * @see https://www.w3.org/TR/2008/REC-xml-20081126/#charsets - XML charset range + * @see http://php.net/manual/en/regexp.reference.escape.php - escape in UTF-8 mode + * + * @param string $xml + * + * @return string + */ + public static function sanitize($xml) + { + return preg_replace('/[^\x{9}\x{A}\x{D}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]+/u', '', $xml); + } +} diff --git a/src/Kernel/Traits/HasHttpRequests.php b/src/Kernel/Traits/HasHttpRequests.php index 023a104..20332cb 100755 --- a/src/Kernel/Traits/HasHttpRequests.php +++ b/src/Kernel/Traits/HasHttpRequests.php @@ -119,11 +119,11 @@ trait HasHttpRequests /** * Make a request. * - * @param string $url + * @param $url * @param string $method - * @param array $options - * - * @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string + * @param array $options + * @return ResponseInterface|\EasyAlipay\Kernel\Support\Collection|array|object|string + * @throws \GuzzleHttp\Exception\GuzzleException */ public function request($url, $method = 'GET', $options = []): ResponseInterface { @@ -138,6 +138,7 @@ trait HasHttpRequests } $response = $this->getHttpClient()->request($method, $url, $options); + $response->getBody()->rewind(); return $response; diff --git a/src/Kernel/Traits/ResponseCastable.php b/src/Kernel/Traits/ResponseCastable.php new file mode 100644 index 0000000..942623f --- /dev/null +++ b/src/Kernel/Traits/ResponseCastable.php @@ -0,0 +1,85 @@ +getBody()->rewind(); + + switch ($type ?? 'array') { + case 'collection': + return $response->toCollection(); + case 'array': + return $response->toArray(); + case 'object': + return $response->toObject(); + case 'raw': + return $response; + default: + if (!is_subclass_of($type, Arrayable::class)) { + throw new InvalidConfigException(sprintf('Config key "response_type" classname must be an instanceof %s', Arrayable::class)); + } + + return new $type($response); + } + } + + /** + * @param mixed $response + * @param string|null $type + * + * @return array|\EasyAlipay\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * + * @throws \EasyAlipay\Kernel\Exceptions\InvalidArgumentException + * @throws \EasyAlipay\Kernel\Exceptions\InvalidConfigException + */ + protected function detectAndCastResponseToType($response, $type = null) + { + switch (true) { + case $response instanceof ResponseInterface: + $response = Response::buildFromPsrResponse($response); + + break; + case $response instanceof Arrayable: + $response = new Response(200, [], json_encode($response->toArray())); + + break; + case ($response instanceof Collection) || is_array($response) || is_object($response): + $response = new Response(200, [], json_encode($response)); + + break; + case is_scalar($response): + $response = new Response(200, [], (string)$response); + + break; + default: + throw new InvalidArgumentException(sprintf('Unsupported response type "%s"', gettype($response))); + } + + return $this->castResponseToType($response, $type); + } +} \ No newline at end of file diff --git a/src/Marketing/Pass/ServiceProvider.php b/src/Marketing/Pass/ServiceProvider.php index 39061c1..c8d2a4a 100755 --- a/src/Marketing/Pass/ServiceProvider.php +++ b/src/Marketing/Pass/ServiceProvider.php @@ -12,7 +12,6 @@ class ServiceProvider implements ServiceProviderInterface */ public function register(Container $app) { - // var_dump($app); $app['pass'] = function ($app) { return new Client($app); }; diff --git a/src/Mini/Risk/ServiceProvider.php b/src/Mini/Risk/ServiceProvider.php index a43bc3f..5e30e4e 100755 --- a/src/Mini/Risk/ServiceProvider.php +++ b/src/Mini/Risk/ServiceProvider.php @@ -12,7 +12,6 @@ class ServiceProvider implements ServiceProviderInterface */ public function register(Container $app) { - // var_dump($app); $app['risk'] = function ($app) { return new Client($app); };