编程

PHP 8.2 新特性 — 新增 Random 扩展

1678 2022-11-14 07:58:10

PHP 8.2 引入一个新的 PHP 扩展叫做 Random 扩展, 这个扩展合并了已有的随机数生成功能,并引入一些新的 PHP 类和异常类,用来提供随机数生成器和细粒度异常处理。

Random 扩展与 PHP 捆绑,不能使用编译时和运行时配置禁用此扩展。Random 扩展在 PHP 8.2 以上版本将会一直可用。

现有的随机数生成功能的修改不会导致跨版本的向下兼容性问题。不过,需要注意的是,检测随机数函数的 PHP 应用和工具(比如,反射 API 中的 ReflectionFunctionReflectionFunction)在 PHP 8.2 会产生不同结果。不过这种场景比较罕见,不太可能在常规的 PHP 应用中出现问题。

现有随机数函数移动到了新增的 Random 扩展中

PHP 在它的标准库中有多个生成随机数的函数。在 PHP 8.2 中,一些函数被移到了 random 扩展。所有这些函数继续常驻在全局命名空间。因为编译 PHP 时 random 扩展始终包含于其中,因此在实际使用中应该不会有不同。

以下这些函数和厂商会被移到 random 扩展。

虽然以上这些函数的功能并没有改变(除了使用新的 random 方面的 ExceptionError 异常之外),这些改变在反射 API 中是可以观察到的。从 PHP 8.2 开始,ReflectionFunction::getExtension 返回 random 扩展而非之前的 standard 标准扩展:

 $reflector = new ReflectionFunction('random_int');
 $reflector->getExtension()->getName();
- // "standard"
+ // "random"

新增 \Random 命名空间

按照 PHP 采用的命名空间政策,所有在 random 扩展引入的新功能被添加到名为 \Random 的命名空间中。

\Random 命名空间为该扩展所保留。已有的 PHP 项目中如果已经用了 \Random 命名空间,只要不和 PHP 8.2 中新增的类、接口、异常产生冲突即可继续使用。

新增 Random\Randomizer 类

Random\Randomizer 是 random 扩展新增的类中最重要的一个。它提供了一个面向对象的 API,用来获取所有随机数生成功能,它使用了伪随机数生成器算法。

下面是使用新类生成随机数的一个例子。

$r = new Random\Randomizer();
echo $r->getInt(1, 100); // 42
echo $r->shuffleBytes('lorem ipsum'); // "ols mpeurim"

更详细的例子,查看用例区。

Random\Randomizer 概要

namespace Random;

final class Randomizer {

    public readonly Engine $engine;

    public function __construct(?Engine $engine = null) {}

    public function nextInt(): int {}

    public function getInt(int $min, int $max): int {}

    public function getBytes(int $length): string {}

    public function shuffleArray(array $array): array {}

    public function shuffleBytes(string $bytes): string {}

    public function pickArrayKeys(array $array, int $num): array {}

    public function __serialize(): array {}

    public function __unserialize(array $data): void {}

}

随机数生成器引擎

PHP 8.2 添加了对多种伪随机生成器算法(PRNG 引擎)的支持。Random\Randomize 接收所有 PRNG 实例,提供了面向对象接口用来使用给定的 PRNG 引擎去生成随机数和随机字节、洗牌数组及从数组中随机选择元素。

Random 扩展提供了所有 PRNG 引擎类都必须执行的 \Random\Engine 接口。此外,还有一个 \Random\CryptoSafeEngine 接口也继承了 \Random\Engine 接口,用于加密操作的安全。

namespace Random;

interface Engine {
    public function generate(): string;
}

interface CryptoSafeEngine extends Engine {
}

Random 扩展提供了 4 个 \Random\Engine 内建实例. 所有这些实现都是 final 类。

Random\Engine\Mt19937

Random\Engine\Mt19937 是 PHP 内建类,实现了 Random\Engine 接口。它与 mt_rand 函数一样都使用了同一个生成器 Mersenne Twister Random Number Generator。它同时也能在类初始化时使用随机值提供算法种子。该引擎不适合于加密安全应用,因为 RNG 的内部状态可以用连续观察输出值获得。 

Random\Engine\Mt19937 概要

namespace Random\Engine;

final class Mt19937 implements \Random\Engine {
    public function __construct(int|null $seed = null, int $mode = MT_RAND_MT19937) {}
    public function generate(): string {}
    public function __serialize(): array {}
    public function __unserialize(array $data): void {}
    public function __debugInfo(): array {}
}

Random\Engine\PcgOneseq128XslRr64

Random\Engine\PcgOneseq128XslRr64 类提供了一个置换同余的生成器实现。该引擎不适用于加密安全应用。

Random\Engine\PcgOneseq128XslRr64 概要

namespace Random\Engine;

final class PcgOneseq128XslRr64 implements \Random\Engine {
    public function __construct(string|int|null $seed = null) {}
    public function generate(): string {}
    public function jump(int $advance): void {}
    public function __serialize(): array {}
    public function __unserialize(array $data): void {}
    public function __debugInfo(): array {}
}

Random\Engine\Xoshiro256StarStar

Random\Engine\Xoshiro256StarStar 类提供了一个 Xoshiro PRNG 的 PHP 实现。虽然 Xoshiro256StarStar 引擎不适用于加密安全应用,因为简单它是提供的这些引擎中最快的,使之成为需要随机数据池的应用的理想候选类(比如随机幻灯片)。

Random\Engine\Xoshiro256StarStar 概要

namespace Random\Engine;

final class Xoshiro256StarStar implements \Random\Engine {
    public function __construct(string|int|null $seed = null) {}
    public function generate(): string {}
    public function jump(): void {}
    public function jumpLong(): void {}
    public function __serialize(): array {}
    public function __unserialize(array $data): void {}
    public function __debugInfo(): array {}
}

Random\Engine\Secure

Random\Engine\Secure 类提供了与 random_bytesrandom_int 函数相同的实现,推荐用于包含加密操作安全的所有随机数生成。

Random\Engine\Secure 概要

namespace Random\Engine;

final class Secure implements \Random\CryptoSafeEngine {
    public function generate(): string {}
}

新增 ExceptionError 类型

作为 radnom 扩展引入和更新的一部分,同时引入了新的 ExceptionError 类型。PRNG 引擎和 Random\Randomizer 类,以及现有的随机数生成函数在 PHP 8.2 及以后的版本中都会抛出更细粒度的异常和错误。

namespace Random;

class RandomError extends \Error {}

class BrokenRandomEngineError extends RandomError {}

class RandomException extends \Exception {}

继承自 PHP 异常
Lists all the current PHP exceptions and errors in a hierarchical order.

已有的 PHP 函数抛出细粒度异常

从 PHP 8.2 起,random_intrandom_bytes 及其他现有函数可以抛出 RandomErrorBrokenRandomEngineErrorRandomException 异常。不过,这不会引入任何向下兼容性问题,因为这些新的异常和错误类型继承自基础的错误和异常类。

用例

下例中使用了  \Random\Randomizer 类及多个引擎,以及一些替代模式

生成 1 到 100 之间的随机数

$randomizer = new Random\Randomizer();
$randomizer->getInt(1, 100); // 42

打乱字符

$randomizer = new Random\Randomizer();
$randomizer->shuffleBytes('abcdef'); // baecfd

请注意 shuffleBytes 方法并非完全如其名所示的那样: 打乱字节(shuffle the bytes)。对多字节字符,会产生乱码/扭曲文本。

打乱数组

$randomizer = new Random\Randomizer();
$randomizer->shuffleArray(['apple', 'banana', 'orange']); // ['orange', 'apple', 'banana']

使用 Mt19937 引擎

$randomizer = new Random\Randomizer(new Random\Engine\Mt19937());
$randomizer->getInt(1, 100); // 68

携带种子使用 Mt19937 引擎

$randomizer = new Random\Randomizer(new Random\Engine\Mt19937(42));
$randomizer->getInt(1, 100); // 43

携带种子使用 Xoshiro256StarStar 引擎

$randomizer = new Random\Randomizer(new Random\Engine\Xoshiro256StarStar(hash("sha256", "some seed value")));
$randomizer->getInt(1, 100); // 43

携带种子使用 PcgOneseq128XslRr64 引擎

$randomizer = new Random\Randomizer(new Random\Engine\PcgOneseq128XslRr64(hash("md5", "some seed value")));
$randomizer->getInt(1, 100); // 43

使用一个返回相同值的模拟引擎,用于单元测试 

class XKCDRandomEngine implements \Random\Engine {
    public function generate(): string {
        return \pack('V', 4); // Chosen by fair dice roll.
                              // Guaranteed to be random.
    }
}
$randomizer = new Random\Randomizer(new XKCDRandomEngine());
$randomizer->getInt(0, 100); // 4

替换 random_bytes 函数调用

- $randomValue = random_bytes(32); // Retrieves 32 random bytes.
+ $randomizer = new Random\Randomizer();
+ $randomizer->getBytes(32);

向后兼容性影响

这是一个新的扩展。现有的应用在 PHP 8.2 以上的版本种应该不会因为 random 扩展出现问题。

不过,使用新的 ExceptionError 类时请慎重,因为老版本在某些情况下会抛出更通用的异常。

可以将部分老版本的功能暂时放在 polyfill 中,不过 PcgOneseq128XslRr64 和 Xoshiro256StarStar 将很难使用这样的方法安全更新,因为依赖的 PRNG 模仿起来十分困难。比如,PcgOneseq128XslRr64 是 128 位的无符号引擎,使用 64 位整数很难效仿。