diff --git a/composer.json b/composer.json index 36896d81c..47cdf92b2 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "require-dev": { "mikey179/vfsstream": "^1.6", "mockery/mockery": "^1.4.4", - "phpstan/phpstan": "^1.0", + "phpstan/phpstan": "^1.0 | ^2", "phpunit/phpunit": "^9.5", "symfony/var-dumper": "^5.2", "jetbrains/phpstorm-attributes": "^1.0", diff --git a/phpstan.neon b/phpstan.neon index aeb3e23c7..f370056db 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,6 +6,12 @@ parameters: ignoreErrors: - identifier: missingType.iterableValue + - + message: '#Property EasyWeChat\\Kernel\\Config::\$items \(array\) does not accept array#' + path: src/Kernel/Config.php + - + message: '#Method EasyWeChat\\Kernel\\Message::format\(\) should return array but returns array#' + path: src/Kernel/Message.php - message: '#\$client .*? does not accept#' path: src/Kernel/HttpClient/AccessTokenAwareClient.php @@ -16,31 +22,34 @@ parameters: message: '#Parameter \#1 \$object of function spl_object_hash expects object, callable given#' path: src/Kernel/Traits/InteractWithHandlers.php - - message: '#Match arm is unreachable because previous comparison is always true#' + message: '#Call to function is_callable\(\) with callable\(\): mixed will always evaluate to true#' path: src/Kernel/Traits/InteractWithHandlers.php - message: '#Parameter \$stable of class EasyWeChat\\MiniApp\\AccessToken constructor expects bool\|null, mixed given#' path: src/MiniApp/Application.php - - message: '#Parameter \#1 \$options of static method EasyWeChat\\Kernel\\HttpClient\\RequestUtil::mergeDefaultRetryOptions\(\) expects array, array given#' + message: '#Parameter \#1 \$options of static method EasyWeChat\\Kernel\\HttpClient\\RequestUtil::mergeDefaultRetryOptions\(\) expects array#' path: src/MiniApp/Application.php + - + message: '#Method EasyWeChat\\MiniApp\\Decryptor::decrypt\(\) should return array but returns array#' + path: src/MiniApp/Decryptor.php - message: '#Parameter \$stable of class EasyWeChat\\OfficialAccount\\(AccessToken|JsApiTicket) constructor expects bool\|null, mixed given#' path: src/OfficialAccount/Application.php - - message: '#Parameter \#1 \$options of static method EasyWeChat\\Kernel\\HttpClient\\RequestUtil::mergeDefaultRetryOptions\(\) expects array, array given#' + message: '#Parameter \#1 \$options of static method EasyWeChat\\Kernel\\HttpClient\\RequestUtil::mergeDefaultRetryOptions\(\) expects array#' path: src/OfficialAccount/Application.php - - message: '#Parameter \#1 \$scopes of method Overtrue\\Socialite\\Providers\\Base::scopes\(\) expects array, array given#' + message: '#Parameter \#1 \$scopes of method Overtrue\\Socialite\\Providers\\Base::scopes\(\) expects array#' path: src/OfficialAccount/Application.php - - message: '#Parameter \#1 \$scopes of method Overtrue\\Socialite\\Providers\\Base::scopes\(\) expects array, array given#' + message: '#Parameter \#1 \$scopes of method Overtrue\\Socialite\\Providers\\Base::scopes\(\) expects array#' path: src/OpenPlatform/Application.php - - message: '#Parameter \#1 \$scopes of method Overtrue\\Socialite\\Providers\\Base::scopes\(\) expects array, array given#' + message: '#Parameter \#1 \$scopes of method Overtrue\\Socialite\\Providers\\Base::scopes\(\) expects array#' path: src/OpenWork/Application.php - - message: '#Parameter \#3 \$defaultOptions of class EasyWeChat\\Pay\\Client constructor expects array, array given#' + message: '#Parameter \#3 \$defaultOptions of class EasyWeChat\\Pay\\Client constructor expects array#' path: src/Pay/Application.php - message: '#Property .*?\$client \(Symfony\\Contracts\\HttpClient\\HttpClientInterface\) does not accept Mockery\\Mock\|Symfony\\Contracts\\HttpClient\\HttpClientInterface#' @@ -49,5 +58,8 @@ parameters: message: '#Method EasyWeChat\\Pay\\Client::createMockClient\(\) should return Mockery\\Mock\|Symfony\\Contracts\\HttpClient\\HttpClientInterface but returns Mockery\\LegacyMockInterface#' path: src/Pay/Client.php - - message: '#Parameter \#1 \$scopes of method Overtrue\\Socialite\\Providers\\Base::scopes\(\) expects array, array given#' + message: '#Call to function is_string\(\) with string will always evaluate to true#' + path: src/Pay/Client.php + - + message: '#Parameter \#1 \$scopes of method Overtrue\\Socialite\\Providers\\Base::scopes\(\) expects array#' path: src/Work/Application.php \ No newline at end of file diff --git a/src/Kernel/HttpClient/AccessTokenExpiredRetryStrategy.php b/src/Kernel/HttpClient/AccessTokenExpiredRetryStrategy.php index 85ed1f171..294153c98 100644 --- a/src/Kernel/HttpClient/AccessTokenExpiredRetryStrategy.php +++ b/src/Kernel/HttpClient/AccessTokenExpiredRetryStrategy.php @@ -15,14 +15,14 @@ class AccessTokenExpiredRetryStrategy extends GenericRetryStrategy protected ?Closure $decider = null; - public function withAccessToken(AccessTokenInterface $accessToken): self + public function withAccessToken(AccessTokenInterface $accessToken): static { $this->accessToken = $accessToken; return $this; } - public function decideUsing(Closure $decider): self + public function decideUsing(Closure $decider): static { $this->decider = $decider; diff --git a/src/Kernel/HttpClient/RequestUtil.php b/src/Kernel/HttpClient/RequestUtil.php index 9b1bd99c4..0137ce343 100644 --- a/src/Kernel/HttpClient/RequestUtil.php +++ b/src/Kernel/HttpClient/RequestUtil.php @@ -104,14 +104,14 @@ public static function formatOptions(array $options, string $method): array } /** - * @param array{headers?:array, xml?:array|string, body?:array|string, json?:array|string} $options + * @param array{headers?:array, xml?:mixed, body?:array|string, json?:mixed} $options * @return array{headers?:array|array>, xml?:array|string, body?:array|string} */ public static function formatBody(array $options): array { $contentType = $options['headers']['Content-Type'] ?? $options['headers']['content-type'] ?? null; - if (isset($options['xml'])) { + if (array_key_exists('xml', $options)) { if (is_array($options['xml'])) { $options['xml'] = Xml::build($options['xml']); } @@ -128,7 +128,7 @@ public static function formatBody(array $options): array unset($options['xml']); } - if (isset($options['json'])) { + if (array_key_exists('json', $options)) { if (is_array($options['json'])) { /** XXX: 微信的 JSON 是比较奇葩的,比如菜单不能把中文 encode 为 unicode */ $options['json'] = json_encode( diff --git a/src/Kernel/HttpClient/RequestWithPresets.php b/src/Kernel/HttpClient/RequestWithPresets.php index 20803592d..65ee9f829 100644 --- a/src/Kernel/HttpClient/RequestWithPresets.php +++ b/src/Kernel/HttpClient/RequestWithPresets.php @@ -126,6 +126,9 @@ public function withFiles(array $files): static return $this; } + /** + * @return array{xml?:array|string,json?:array|string,body?:array|string,query?:array,headers?:array} + */ public function mergeThenResetPrepends(array $options, string $method = 'GET'): array { $name = in_array(strtoupper($method), ['GET', 'HEAD', 'DELETE']) ? 'query' : 'body'; diff --git a/src/Kernel/Support/Arr.php b/src/Kernel/Support/Arr.php index 3775e9ab0..a7377663e 100644 --- a/src/Kernel/Support/Arr.php +++ b/src/Kernel/Support/Arr.php @@ -46,7 +46,7 @@ public static function exists(array $array, string|int $key): bool /** * @param array $array - * @return array + * @return array */ public static function set(array &$array, string|int|null $key, mixed $value): array { diff --git a/src/Kernel/Support/PublicKey.php b/src/Kernel/Support/PublicKey.php index 88c30de26..73c707d73 100644 --- a/src/Kernel/Support/PublicKey.php +++ b/src/Kernel/Support/PublicKey.php @@ -26,7 +26,7 @@ public function getSerialNo(): string { $info = openssl_x509_parse($this->certificate); - if ($info === false || ! isset($info['serialNumberHex'])) { + if ($info === false) { throw new InvalidConfigException('Read the $certificate failed, please check it whether or nor correct'); } diff --git a/src/Kernel/Support/UserAgent.php b/src/Kernel/Support/UserAgent.php index 560a72a40..0d534307d 100644 --- a/src/Kernel/Support/UserAgent.php +++ b/src/Kernel/Support/UserAgent.php @@ -9,6 +9,7 @@ use function array_map; use function array_unshift; use function class_exists; +use function constant; use function curl_version; use function defined; use function explode; @@ -26,7 +27,7 @@ public static function create(array $appends = []): string $value = array_map('strval', $appends); if (defined('HHVM_VERSION')) { - array_unshift($value, 'HHVM/'.HHVM_VERSION); + array_unshift($value, 'HHVM/'.constant('HHVM_VERSION')); } $disabledFunctions = explode(',', ini_get('disable_functions') ?: ''); diff --git a/src/Kernel/Traits/InteractWithHandlers.php b/src/Kernel/Traits/InteractWithHandlers.php index f58660033..7d0402e7f 100644 --- a/src/Kernel/Traits/InteractWithHandlers.php +++ b/src/Kernel/Traits/InteractWithHandlers.php @@ -69,13 +69,13 @@ public function createHandlerItem(callable|string $handler): array /** * @throws InvalidArgumentException */ - protected function getHandlerHash(callable|string $handler): string + protected function getHandlerHash(callable|array|string $handler): string { return match (true) { is_string($handler) => $handler, - is_array($handler) => is_string($handler[0]) ? $handler[0].'::'.$handler[1] : get_class( - $handler[0] - ).$handler[1], + is_array($handler) => is_string($handler[0]) + ? $handler[0].'::'.$handler[1] + : get_class($handler[0]).$handler[1], $handler instanceof Closure => spl_object_hash($handler), is_callable($handler) => spl_object_hash($handler), default => throw new InvalidArgumentException('Invalid handler: '.gettype($handler)), diff --git a/src/Kernel/Traits/InteractWithHttpClient.php b/src/Kernel/Traits/InteractWithHttpClient.php index b7b8d80d6..638ea3d1f 100644 --- a/src/Kernel/Traits/InteractWithHttpClient.php +++ b/src/Kernel/Traits/InteractWithHttpClient.php @@ -8,11 +8,11 @@ use EasyWeChat\Kernel\HttpClient\ScopingHttpClient; use EasyWeChat\Kernel\Support\Arr; use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; use function is_array; -use function property_exists; trait InteractWithHttpClient { @@ -32,8 +32,7 @@ public function setHttpClient(HttpClientInterface $httpClient): static $this->httpClient = $httpClient; if ($this instanceof LoggerAwareInterface && $httpClient instanceof LoggerAwareInterface - && property_exists($this, 'logger') - && $this->logger) { + && $this->logger instanceof LoggerInterface) { $httpClient->setLogger($this->logger); } diff --git a/src/OfficialAccount/AccessToken.php b/src/OfficialAccount/AccessToken.php index 369bf073c..d21a38a85 100644 --- a/src/OfficialAccount/AccessToken.php +++ b/src/OfficialAccount/AccessToken.php @@ -77,7 +77,7 @@ public function getToken(): string } /** - * @return array + * @return array{access_token:string} * * @throws HttpException * @throws InvalidArgumentException diff --git a/src/Pay/Client.php b/src/Pay/Client.php index a1d42af1e..f134bd5fb 100644 --- a/src/Pay/Client.php +++ b/src/Pay/Client.php @@ -33,6 +33,7 @@ use function is_array; use function is_string; use function str_starts_with; +use function strcasecmp; /** * @method ResponseInterface get(string $uri, array $options = []) @@ -54,7 +55,7 @@ class Client implements HttpClientInterface use RequestWithPresets; /** - * @var array + * @var array{base_uri:string,headers:array{'Content-Type':string,Accept:string}} */ protected array $defaultOptions = [ 'base_uri' => 'https://api.mch.weixin.qq.com/', @@ -64,13 +65,23 @@ class Client implements HttpClientInterface ], ]; - public const V3_URI_PREFIXES = [ + protected const V3_URI_PREFIXES = [ '/v3/', - '/sandbox/v3/', '/hk/v3/', '/global/v3/', ]; + /** + * Special absolute path string over `GET` method + */ + protected const V2_URI_OVER_GETS = [ + '/appauth/getaccesstoken', // secret API which's respond `JSON`, must keep in the first + '/papay/entrustweb', + '/papay/h5entrustweb', + '/papay/partner/entrustweb', + '/papay/partner/h5entrustweb', + ]; + protected bool $throw = true; /** @@ -101,7 +112,6 @@ public function __construct( */ public function request(string $method, string $url, array $options = []): ResponseInterface { - /** @var array{headers?:array, xml?:array|string, body?:array|string} $options */ if (empty($options['headers'])) { $options['headers'] = []; } @@ -118,8 +128,7 @@ public function request(string $method, string $url, array $options = []): Respo $options['headers']['Authorization'] = $this->createSignature($method, $url, $_options); } } else { - // v2 全部为 xml 请求 - if (! empty($options['xml'])) { + if (! strcasecmp($method, 'POST') && ! empty($options['xml'])) { if (is_array($options['xml'])) { $options['xml'] = Xml::build($this->attachLegacySignature($options['xml'])); } @@ -136,6 +145,10 @@ public function request(string $method, string $url, array $options = []): Respo $options['body'] = Xml::build($this->attachLegacySignature($options['body'])); } + if (! strcasecmp($method, 'GET') && in_array($url, self::V2_URI_OVER_GETS) && is_array($options['query'] ?? null)) { + $options['query'] = $this->attachLegacySignature($options['query']); + } + if (! isset($options['headers']['Content-Type']) && ! isset($options['headers']['content-type'])) { $options['headers']['Content-Type'] = 'text/xml'; } @@ -148,17 +161,29 @@ public function request(string $method, string $url, array $options = []): Respo return new Response( $this->client->request($method, $url, $options), - failureJudge: $this->isV3Request($url) ? null : fn (Response $response) => $response->toArray()['return_code'] === 'FAIL' || $response->toArray()['result_code'] === 'FAIL', + failureJudge: $this->isV3Request($url) ? null : function (Response $response) use ($url): bool { + $arr = $response->toArray(); + + return ! ( + $url === self::V2_URI_OVER_GETS[0] && array_key_exists('retcode', $arr) && $arr['retcode'] === 0 + ) || ! ( + // protocol code, most similar to the HTTP status code in APIv3 + array_key_exists('return_code', $arr) && $arr['return_code'] === 'SUCCESS' + ) || ( + // business code, most similar to the Response.JSON.code in APIv3 + array_key_exists('result_code', $arr) && $arr['result_code'] !== 'SUCCESS' + ); + }, throw: $this->throw ); } protected function isV3Request(string $url): bool { - $uri = new Uri($url); + $uri = (new Uri($url))->getPath(); foreach (self::V3_URI_PREFIXES as $prefix) { - if (str_starts_with('/'.ltrim($uri->getPath(), '/'), $prefix)) { + if (str_starts_with($uri, $prefix)) { return true; } } @@ -166,7 +191,7 @@ protected function isV3Request(string $url): bool return false; } - public function withSerialHeader(?string $serial = null): self + public function withSerialHeader(?string $serial = null): static { $platformCerts = $this->merchant->getPlatformCerts(); if (empty($platformCerts)) { diff --git a/src/Pay/Merchant.php b/src/Pay/Merchant.php index 1da21300d..27e1c18ca 100644 --- a/src/Pay/Merchant.php +++ b/src/Pay/Merchant.php @@ -74,7 +74,7 @@ public function getPlatformCerts(): array } /** - * @param array $platformCerts + * @param array $platformCerts * @return array * * @throws InvalidArgumentException diff --git a/src/Pay/Server.php b/src/Pay/Server.php index 5f8aa3cf2..7b6d69c50 100644 --- a/src/Pay/Server.php +++ b/src/Pay/Server.php @@ -22,6 +22,7 @@ use function is_array; use function json_decode; use function json_encode; +use function str_contains; use function strval; /** @@ -115,8 +116,8 @@ public function getRequestMessage(?ServerRequestInterface $request = null): \Eas $originContent = (string) ($request ?? $this->getRequest())->getBody(); // 微信支付的回调数据回调,偶尔是 XML https://github.com/w7corp/easywechat/issues/2737 - // PS: 这帮傻逼,真的是该死啊 - $isXml = str_starts_with($originContent, 'getRequest())->getHeaderLine('content-type'); + $isXml = (str_contains($contentType, 'text/xml') || str_contains($contentType, 'application/xml')) && str_starts_with($originContent, 'decodeXmlMessage($originContent) : $this->decodeJsonMessage($originContent); return new Message($attributes, $originContent); @@ -159,11 +160,11 @@ protected function decodeJsonMessage(string $contents): array { $attributes = json_decode($contents, true); - if (! is_array($attributes)) { + if (! (is_array($attributes) && is_array($attributes['resource']))) { throw new RuntimeException('Invalid request body.'); } - if (empty($attributes['resource']['ciphertext'])) { + if (empty($attributes['resource']['ciphertext'] ?? null)) { throw new RuntimeException('Invalid request.'); }