SDK目标是优先APIv3
版,当然也需要考虑当下,APIv2
还在并行运行。两者之间有共性也有特性,把共性部分抽象出来,当属格式化
参数部分。随机字符串
首当其冲,那就从这个函数实现开始吧。
本博发布之日,这个函数已经迭代了一个版本,并且加入了测试用例覆盖,最终形态如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?php
/**
* Generate a random ASCII string aka `nonce`, similar as `random_bytes`.
*
* @param int $size - Nonce string length, default is 32.
*
* @return string - base62 random string.
*/
public static function nonce(int $size = 32): string
{
return array_reduce(range(1, $size), static function(string $char) {
return $char .= 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'[mt_rand(0, 61)];
}, '');
}
|
本来是可以一行显示的,为了阅读起来方便,特意做了格式化,这个函数使用了PHP内置的三个函数array_reduce
, range
及 mt_rand
,并且使用了 Closure
Static Anonymous Functions
。
此函数的设计思路是:根据入参$size
,先构建一个堆栈,然后从 Base62
字符串内随机取个数,填充堆栈并合并返回。
上一版是这么实现的:
1
2
3
4
5
6
7
8
9
|
<?php
const BASE62_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
public static function nonce(int $size = 32): string
{
return preg_replace_callback('#0#', static function() {
return BASE62_CHARS[rand(0, 61)];
}, str_repeat('0', $size));
}
|
使用了 preg_replace_callback
rand
及 str_repeat
三个内置函数,同样的设计思路,不过有个缺陷,即 $size
型参必须大于0,这个是受限 str_repeat
型参要求。
两个函数其实都备注有与PHP内置的 random_bytes
函数相似,而 random_bytes
也存在型参 $size
必须大于0的要求,其返回值是 Base36
的。
最终选择 array_reduce+range+mt_rand
的组合,这里是做了性能测试的,测试代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
// file: cli.php, run: php -f cli.php
<?php
const BASE62_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$start = microtime(true);
$a = '';
for ($i = 0; $i < 50; $i++) {
$a .= BASE62_CHARS[mt_rand(0, 61)];
}
printf(
'[%30s] Time: %.7f s %-32.32s %s', 'for loop',
microtime(true) - $start, $a, PHP_EOL
);
$start = microtime(true);
$a = preg_replace_callback('#0#', static function() {
return BASE62_CHARS[rand(0, 61)];
}, str_repeat('0', 50));
printf(
'[%30s] Time: %.7f s %-32.32s %s', 'preg_replace_callback',
microtime(true) - $start, $a, PHP_EOL
);
$start = microtime(true);
$a = array_reduce(range(1, 50), static function(string $c) {
return $c .= '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'[random_int(0, 61)];
}, '');
printf(
'[%30s] Time: %.7f s %-32.32s %s', 'array_reduce/random_int',
microtime(true) - $start, $a, PHP_EOL
);
$start = microtime(true);
$a = array_reduce(range(1, 50), static function(string $c) {
return $c .= '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'[mt_rand(0, 61)];
}, '');
printf(
'[%30s] Time: %.7f s %-32.32s %s', 'array_reduce/mt_rand',
microtime(true) - $start, $a, PHP_EOL
);
$start = microtime(true);
$a = substr(bin2hex(random_bytes(50)), 0, 50);
printf(
'[%30s] Time: %.7f s %-32.32s %s', 'random_bytes',
microtime(true) - $start, $a, PHP_EOL
);
|
跑上四圈的结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
[ for loop] Time: 0.0004230 s twqdlCDCyMknrFMJEjPfl94SdFK2a2Km
[ preg_replace_callback] Time: 0.0035770 s aTb7yrpFVitQju7sKAgofTuiSsiw1Top
[ array_reduce/random_int] Time: 0.0001869 s 9IkLR7HEryOy8zlzuxpVIBpttpprlNib
[ array_reduce/mt_rand] Time: 0.0000098 s 7j2F2stRXiHvAxQ4j2IhplHXCHMGtl9j
[ substr/bin2hex/random_bytes] Time: 0.0000100 s e6e33fd212b3083bfc4ebb29a13710b1
[ for loop] Time: 0.0000420 s UppY2CoJOVSmHE0EkynhyOeqaWdvSGJ5
[ preg_replace_callback] Time: 0.0002630 s s64pxnpKALGq16HxnbCbKh0NGgRq8Ou2
[ array_reduce/random_int] Time: 0.0001841 s 7yYaGsWoBudf5PKlz2OkdodGrN05OkQo
[ array_reduce/mt_rand] Time: 0.0000188 s 0ruErA844vf9CifmfhEiyjoUVcbgv3yy
[ substr/bin2hex/random_bytes] Time: 0.0000091 s db57e8c90751d00b76da5d553f54c950
[ for loop] Time: 0.0000441 s 0hZgmb4EeYuL14FDd0UIkbjiNyjGjZxy
[ preg_replace_callback] Time: 0.0002651 s BMoSsbN2N7ObEDmqpgtTKKtGdMiUuV4U
[ array_reduce/random_int] Time: 0.0001869 s 7cGZKBUGiZL6v594o5k6wtQTmm5I7IYq
[ array_reduce/mt_rand] Time: 0.0000200 s leXlVr5aJ8zmhbn9kk27D3bpy4FJ6Mhy
[ substr/bin2hex/random_bytes] Time: 0.0000100 s 7c9de4e1ee533aa3c9ddecf78667afaf
[ for loop] Time: 0.0000479 s OpqudO593AKmRROllDX8h0tjuBzLXPQZ
[ preg_replace_callback] Time: 0.0002871 s oXoJEBRt0Qpy5jXUNDnAapblbXhpBFI3
[ array_reduce/random_int] Time: 0.0002170 s SPdIjbbiI3wKLrMCnAiurpBdnxeJsip6
[ array_reduce/mt_rand] Time: 0.0000169 s bKygHiTtIj6LsnmWxRKXTtSpr1oIdkBJ
[ substr/bin2hex/random_bytes] Time: 0.0000169 s f8c4105678ba0e97f41fbb9197332f28 |
其中 preg_replace_callback
组合是最慢的, array_reduce/mt_rand
组合与最快的 random_bytes
很接近。
array_reduce/mt_rand
接受的入参可以是负数,也可以是正数,从严谨性上来取舍,遂选择了这个组合。
测试用例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
<?php
public function nonceRulesProvider(): array
{
return [
'default $size=32' => [32, '/[a-zA-Z0-9]{32}/'],
'half-default $size=16' => [16, '/[a-zA-Z0-9]{16}/'],
'hundred $size=100' => [100, '/[a-zA-Z0-9]{100}/'],
'one $size=1' => [1, '/[a-zA-Z0-9]{1}/'],
'zero $size=0' => [0, '/[a-zA-Z0-9]{2}/'],
'negative $size=-1' => [-1, '/[a-zA-Z0-9]{3}/'],
'negative $size=-16' => [-16, '/[a-zA-Z0-9]{18}/'],
'negative $size=-32' => [-32, '/[a-zA-Z0-9]{34}/'],
];
}
/**
* @dataProvider nonceRulesProvider
*/
public function testNonce(int $size, string $pattern): void
{
$nonce = Formatter::nonce($size);
self::assertIsString($nonce);
self::assertTrue(strlen($nonce) === ($size > 0 ? $size : abs($size - 2)));
if (method_exists($this, 'assertMatchesRegularExpression')) {
self::assertMatchesRegularExpression($pattern, $nonce);
} else {
self::assertRegExp($pattern, $nonce);
}
}
|
适应性上堪称完美。
一个重要提示: 按照PHP手册上提示,mt_rand
是密码学不安全的函数,这里也做一并提示 Formatter::nonce()
也是密码学不安全实现。本类库在使用时,仅当salt
(盐)使用,扩展使用时,请注意使用场景。
这个函数是对time()
的一个及其简单的一个封装。之所以要封装,其实是有一点点说法的。按照微信支付官方开发文档说明,时间戳是自1970年1月1日起的Unix timesamp
,即 Epoch timesamp。PHP内置time()
函数就是这个值。其他平台,有见到把timesamp
翻译成yyyy-MM-dd HH:mm:ss
格式的字符串,做这么个封装的原因:
- 对函数返回值做类型签名,严格区分其他平台的翻译;
- PHP的命名空间
namespace
,存在FQN
引用,自PHP7
开始,在命名空间下可以use function
,为让代码通俗易懂,在一个地方引用内置函数,比多次引用要直观;
代码块如下
1
2
3
4
5
6
7
8
9
10
|
<?php
/**
* Retrieve the current `Unix` timestamp.
*
* @return int - Epoch timestamp.
*/
public static function timestamp(): int
{
return time();
}
|
测试用例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<?php
public function testTimestamp(): void
{
$timestamp = Formatter::timestamp();
$pattern = '/^1[0-9]{9}/';
self::assertIsInt($timestamp);
$timestamp = strval($timestamp);
self::assertTrue(strlen($timestamp) === 10);
if (method_exists($this, 'assertMatchesRegularExpression')) {
self::assertMatchesRegularExpression($pattern, $timestamp);
} else {
self::assertRegExp($pattern, $timestamp);
}
}
|
1开头的10位纯数字,记住了,这就是 Unix timestamp
。
这个函数,官方文档说的很详细了,但是内涵相当多的知识点,没有明说(哲学:我认为你应该知道所以我不讲了),这个函数就是其中之一。翻MDN,引申有说明,RFC 7235, section 4.2: Authorization:只要符合规范,厂商可自主实现<type> <credentials>
。
微信支付 APIv3
即以 WECHATPAY2-SHA256-RSA2048
声明 type
变量,mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"
组合实现 credentials
声明。
代码块如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<?php
/**
* Formatting for the heading `Authorization` value.
*
* @param string $mchid - The merchant ID.
* @param string $nonce - The Nonce string.
* @param string $signature - The base64-encoded `Rsa::sign` ciphertext.
* @param string $timestamp - The `Unix` timestamp.
* @param string $serial - The serial number of the merchant public certification.
*
* @return string - The APIv3 Authorization `header` value
*/
public static function authorization(string $mchid, string $nonce, string $signature, string $timestamp, string $serial): string
{
return sprintf(
'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"',
$mchid, $nonce, $signature, $timestamp, $serial
);
}
|
那些官方没有明说的知识点就来了:
- 商户号
mchid
是1至32字符的base62
字符串,当前绝大部分商户号是纯数字;
- 请求随机串
nonce_str
只要能通过HTTP上送的字符均可,建议是至少16字符的base62
字符串;
- 签名值
signature
是base64
字符串,base64末尾有可能有0个、1个或者2个=
号,这都是正常的base64
字符串;
- 时间戳
timestamp
是 unix timestamp
,如 Formatter::timestamp()
封装所述;
- 商户API证书
serial_no
是8至40字符的【0-9A-Z]
全大写字符串;
- 认证值有字典要求,即
mchid,nonce,signature,timestamp,serial
即这5个必须出现,排列组合顺序任意,值的组合用半角逗号(,)分隔,严格没有空格;
我们用测试用例覆盖来校验如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?php
public function testAuthorization(): void
{
$value = Formatter::authorization('1001', Formatter::nonce(), 'mock', (string) Formatter::timestamp(), 'mockmockmock');
self::assertIsString($value);
self::assertStringStartsWith('WECHATPAY2-SHA256-RSA2048 ', $value);
self::assertStringEndsWith('"', $value);
$pattern = '/^WECHATPAY2-SHA256-RSA2048 '
. 'mchid="[0-9A-Za-z]{1,32}",'
. 'nonce_str="[0-9A-Za-z]{16,}",'
. 'signature="[0-9A-Za-z\+\/]+={0,2}",'
. 'timestamp="1[0-9]{9}",'
. 'serial_no="[0-9A-Z]{8,40}"$/';
if (method_exists($this, 'assertMatchesRegularExpression')) {
self::assertMatchesRegularExpression($pattern, $value);
} else {
self::assertRegExp($pattern, $value);
}
}
|
PS: 官方文档上,特意说了 认证类型 <type>
,目前为 WECHATPAY2-SHA256-RSA2048
,畅想应该还会有其他值。所以在APIv3
开发的时候,建议还是要多看看官方文档/公告说明,以免 我的代码没动过啊,为什么现在不行了
这类问题产生。
顾名思义,这个函数就是用来格式化请求参数的,按照官方开发文档介绍,输入参数是需要按照”\n”(char0x0A
)做排列合并,而0x0A
是个非打印字符,文本描述起来比较困难,另外对于空请求串情况,占位行不能省略,顾做一个纵向封装,以程序语言(函数型参)来描述请求串,代码块如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<?php
/**
* Formatting this `HTTP::request` for `Rsa::sign` input.
*
* @param string $method - The HTTP verb, must be the uppercase sting.
* @param string $uri - Combined string with `URL::pathname` and `URL::search`.
* @param string $timestamp - The `Unix` timestamp, should be the one used in `authorization`.
* @param string $nonce - The `Nonce` string, should be the one used in `authorization`.
* @param string $body - The playload string, HTTP `GET` should be an empty string.
*
* @return string - The content for `Rsa::sign`
*/
public static function request(string $method, string $uri, string $timestamp, string $nonce, string $body = ''): string
{
return static::joinedByLineFeed($method, $uri, $timestamp, $nonce, $body);
}
|
函数入参接受5部分,其中$body
可为空,内置驱动 joinedByLineFeed
做参数合并并返回字符串。这里有个点就是对入参 $timestamp
的类型定义,这里用了字符串定义,原因是:合并后的输出是个字符串,输入端就做了*妥协*,当然在非严格限制模式行,用纯数字输入也是可以的。
测试用例覆盖如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
<?php
const LINE_FEED = "\n";
public function requestPhrasesProvider(): array
{
return [
'DELETE root(/)' => ['DELETE', '/', ''],
'DELETE root(/) with query' => ['DELETE', '/?hello=wechatpay', ''],
'GET root(/)' => ['GET', '/', ''],
'GET root(/) with query' => ['GET', '/?hello=wechatpay', ''],
'POST root(/) with body' => ['POST', '/', '{}'],
'POST root(/) with body and query' => ['POST', '/?hello=wechatpay', '{}'],
'PUT root(/) with body' => ['PUT', '/', '{}'],
'PUT root(/) with body and query' => ['PUT', '/?hello=wechatpay', '{}'],
'PATCH root(/) with body' => ['PATCH', '/', '{}'],
'PATCH root(/) with body and query' => ['PATCH', '/?hello=wechatpay', '{}'],
];
}
/**
* @dataProvider requestPhrasesProvider
*/
public function testRequest(string $method, string $uri, string $body): void
{
$value = Formatter::request($method, $uri, (string) Formatter::timestamp(), Formatter::nonce(), $body);
self::assertIsString($value);
self::assertStringStartsWith($method, $value);
self::assertStringEndsWith(LINE_FEED, $value);
self::assertLessThanOrEqual(substr_count($value, LINE_FEED), 5);
$pattern = '#^' . $method . LINE_FEED
. preg_quote($uri) . LINE_FEED
. '1[0-9]{9}' . LINE_FEED
. '[0-9A-Za-z]{32}' . LINE_FEED
. preg_quote($body) . LINE_FEED
. '$#';
if (method_exists($this, 'assertMatchesRegularExpression')) {
self::assertMatchesRegularExpression($pattern, $value);
} else {
self::assertRegExp($pattern, $value);
}
}
|
测试用例说明一下,共计十种情形,函数返回值,必须以所请求的 $method
开头,并且至少含有5个LINE_FEED
常量,其中一个LINE_FEED
在末尾。 用例的数据供给,含了已知的APIv3
HTTP verbs,即:DELETE
, GET
, POST
, PUT
及 PATCH
。 按照 rfc3986 规范,DELETE
及GET
是不带请求$body
的。
这个函数是APIv3
开发文档上验签逻辑的一个封装,直白代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?php
/**
* Formatting this `HTTP::response` for `Rsa::verify` input.
*
* @param string $timestamp - The `Unix` timestamp, should be the one from `response::headers[Wechatpay-Timestamp]`.
* @param string $nonce - The `Nonce` string, should be the one from `response::headers[Wechatpay-Nonce]`.
* @param string $body - The response payload string, HTTP status(`201`, `204`) should be an empty string.
*
* @return string - The content for `Rsa::verify`
*/
public static function response(string $timestamp, string $nonce, string $body = ''): string
{
return static::joinedByLineFeed($timestamp, $nonce, $body);
}
|
测试用例覆盖如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
<?php
public function responsePhrasesProvider(): array
{
return [
'HTTP 200 STATUS with body' => ['{}'],
'HTTP 200 STATUS with no body' => [''],
'HTTP 202 STATUS with no body' => [''],
'HTTP 204 STATUS with no body' => [''],
'HTTP 301 STATUS with no body' => [''],
'HTTP 301 STATUS with body' => ['<html></html>'],
'HTTP 302 STATUS with no body' => [''],
'HTTP 302 STATUS with body' => ['<html></html>'],
'HTTP 307 STATUS with no body' => [''],
'HTTP 307 STATUS with body' => ['<html></html>'],
'HTTP 400 STATUS with body' => ['{}'],
'HTTP 401 STATUS with body' => ['{}'],
'HTTP 403 STATUS with body' => ['<html></html>'],
'HTTP 404 STATUS with body' => ['<html></html>'],
'HTTP 500 STATUS with body' => ['{}'],
'HTTP 502 STATUS with body' => ['<html></html>'],
'HTTP 503 STATUS with body' => ['<html></html>'],
];
}
/**
* @dataProvider responsePhrasesProvider
*/
public function testResponse(string $body): void
{
$value = Formatter::response((string) Formatter::timestamp(), Formatter::nonce(), $body);
self::assertIsString($value);
self::assertStringEndsWith(LINE_FEED, $value);
self::assertLessThanOrEqual(substr_count($value, LINE_FEED), 3);
$pattern = '#^1[0-9]{9}' . LINE_FEED
. '[0-9A-Za-z]{32}' . LINE_FEED
. preg_quote($body) . LINE_FEED
. '$#';
if (method_exists($this, 'assertMatchesRegularExpression')) {
self::assertMatchesRegularExpression($pattern, $value);
} else {
self::assertRegExp($pattern, $value);
}
}
|
知识点如下:
- HTTP请求返回内容,均是纯文本格式,即使是头部内容,解析出来也是字符串形式;
- 与请求串格式化函数很像,这里接受3个参数,并且使用内置抽象函数
joinedByLineFeed
做数据合并;
- 官方文档上,仅描述了
204
状态码时的返回内容为空,其实API接口,也有可能产生202
状态码返回,内容也是空的;
301/302/307/403/404/502/503
等状态码时,返回的内容有可能不是预期的json
字符串,而是html
串;
$timestamp
/$nonce
是从HTTP HEADES上取,对应的key是Wechatpay-Timestamp
及Wechatpay-Nonce
,其实还有3个key非常有用,后边再讲;
上边两个函数,都提到这个字符串合并函数了,之所以单独拎出来,是因为这个函数不仅仅用在这两个函数之上,微信字符与微信的数据交换二次签名
以及官方小程序发券
插件数据签名,数据格式均以0x0A
做合并签名,并且末行的0x0A
是不能少的。
这里不得不对APIv3
的规范性做个感性评价,一致性还是相当可以的(虽然有难以言表的接口出现,拔特总体还是很可以的)。
代码块如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
<?php
/**
* Joined this inputs by for `Line Feed`(LF) char.
*
* @param string[] ...$pieces - The string(s) joined by line feed.
*
* @return string - The joined string.
*/
public static function joinedByLineFeed(...$pieces): string
{
return implode("\n", array_merge($pieces, ['']));
}
|
这里用到了PHP7
的弹性入参功能Variable-length argument lists
,入参是平展展的字符串,赋值给$piecies
型参,内部用了内置的implode
及array_merge
函数,来构建末尾是0xoA
的字符串。
测试用例如下(其实request/response都已经覆盖):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
<?php
public function joinedByLineFeedPhrasesProvider(): array
{
return [
'one argument' => [1],
'two arguments' => [1, '2'],
'mixed arguments' => [1, 2.0, '3', LINE_FEED, true, false, null, '4'],
];
}
/**
* @dataProvider joinedByLineFeedPhrasesProvider
*/
public function testJoinedByLineFeed(...$data): void
{
$value = Formatter::joinedByLineFeed(...$data);
self::assertIsString($value);
self::assertStringEndsWith(LINE_FEED, $value);
self::assertLessThanOrEqual(substr_count($value, LINE_FEED), count($data));
}
public function testNoneArgumentPassedToJoinedByLineFeed(): void
{
$value = Formatter::joinedByLineFeed();
self::assertIsString($value);
self::assertStringNotContainsString(LINE_FEED, $value);
self::assertTrue(strlen($value) == 0);
}
|
小技巧: 在测试用例上,使用了PHP7
的变长函数参数特性,透传给了被测试函数(不一定是个好方案),姑且先这样测试,后续再说。
这个函数是APIv2
用的,字典序排序入参,代码块如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?php
/**
* Sort an array by key with `SORT_FLAG_CASE | SORT_NATURAL` flag.
*
* @param array<string, string|int> $thing - The input array.
*
* @return array<string, string|int> - The sorted array.
*/
public static function ksort(array $thing = []): array
{
ksort($thing, SORT_FLAG_CASE | SORT_NATURAL);
return $thing;
}
|
知识点: PHP
的 SORT_NATURAL
是按自然序排序,让我们用测试用例来感受一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
<?php
public function ksortByFlagNaturePhrasesProvider(): array
{
return [
[
['a' => '1', 'b' => '3', 'aa' => '2'],
['a' => '1', 'aa' => '2', 'b' => '3'],
],
[
['rfc1' => '1', 'b' => '4', 'rfc822' => '2', 'rfc2086' => '3'],
['b' => '4', 'rfc1' => '1', 'rfc822' => '2', 'rfc2086' => '3'],
],
];
}
/**
* @dataProvider ksortByFlagNaturePhrasesProvider
*/
public function testKsort(array $thing, array $excepted): void
{
self::assertEquals(Formatter::ksort($thing), $excepted);
}
public function nativeKsortPhrasesProvider(): array
{
return [
[
['a' => '1', 'b' => '3', 'aa' => '2'],
['a' => '1', 'aa' => '2', 'b' => '3'],
],
[
['rfc1' => '1', 'b' => '4', 'rfc822' => '2', 'rfc2086' => '3'],
['b' => '4', 'rfc1' => '1', 'rfc2086' => '3', 'rfc822' => '2'],
],
];
}
/**
* @dataProvider nativeKsortPhrasesProvider
*/
public function testNativeKsort(array $thing, array $excepted): void
{
ksort($thing);
self::assertEquals($thing, $excepted);
}
|
差异点就在于,对于字符串+数字序列的键值,自然排序是rfc1, rfc822, rfc2086
,默认排序是rfc1, rfc2086, rfc822
,理论上,这个和官方的字典序
排序是不完全一样的,待后续观察。
方法名儿已经语义化了,是跟querystring
做法类似,但是是有排除,排除规则是:键值是sign
的,值是空串或者null
的。代码块如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?php
/**
* Like `queryString` does but without the `sign` and `empty value` entities.
*
* @param array<string, string|int|null> $thing - The input array.
*
* @return string - The `key=value` pair string whose joined by `&` char.
*/
public static function queryStringLike(array $thing = []): string
{
$data = [];
foreach ($thing as $key => $value) {
if ($key === 'sign' || is_null($value) || $value === '') {
continue;
}
$data[] = implode('=', [$key, $value]);
}
return implode('&', $data);
}
|
为什么要排除掉null
呢??这其实是本SDK一个强制要求,APIv2
上,有一个接口是不太标准的,要求入参是NULL
字符串,null
数据类型和NULL
字符串不是一回事儿,不是一回事儿,不是一回事儿,重要的事情说三遍。。。。
测试用例覆盖如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
<?php
/**
* @dataProvider nativeKsortPhrasesProvider
*/
public function testNativeKsort(array $thing, array $excepted): void
{
self::assertTrue(ksort($thing));
self::assertEquals($thing, $excepted);
}
public function queryStringLikePhrasesProvider(): array
{
return [
'none specific chars' => [
['a' => '1', 'b' => '3', 'aa' => '2'],
'a=1&b=3&aa=2',
],
'has `sign` key' => [
['a' => '1', 'b' => '3', 'sign' => '2'],
'a=1&b=3',
],
'has `empty` value' => [
['a' => '1', 'b' => '3', 'c' => ''],
'a=1&b=3',
],
'has `null` value' => [
['a' => '1', 'b' => null, 'c' => '2'],
'a=1&c=2',
],
'mixed `sign` key, `empty` and `null` values' => [
['bob' => '1', 'alice' => null, 'tom' => '', 'sign' => 'mock'],
'bob=1',
],
];
}
/**
* @dataProvider queryStringLikePhrasesProvider
*/
public function testQueryStringLike(array $thing, string $excepted): void
{
$value = Formatter::queryStringLike($thing);
self::assertIsString($value);
self::assertEquals($value, $excepted);
}
|
至此,SDK上封装的基础格式化参数函数讲解完了,知识点即“踩坑”点,规集整理出来,分享给大家。