简介

PHP 8.1 引入了许多重要的新特性,包括枚举(Enums)、只读属性(Readonly Properties)、一等可调用语法(First-class Callable Syntax)、Fibers、交叉类型(Intersection Types)等。本文将介绍主要变化和如何从 PHP 8.0 迁移到 PHP 8.1。

参考资源

PHP 8.1 新特性

枚举(Enums)

PHP 8.1 原生支持枚举类型,取代了之前使用类常量的方式:

// 纯枚举(Pure Enum)
enum Status {
    case Draft;
    case Published;
    case Archived;
}

// 回退枚举(Backed Enum)- 带有标量值
enum Status: string {
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
}

// 使用枚举作为类型提示
class BlogPost {
    public function __construct(
        public Status $status = Status::Draft
    ) {}
}

$post = new BlogPost();
echo $post->status->value; // 'draft'

// 枚举可以有方法
enum Color: string {
    case Red = '#FF0000';
    case Green = '#00FF00';
    case Blue = '#0000FF';

    public function label(): string {
        return match($this) {
            self::Red => '红色',
            self::Green => '绿色',
            self::Blue => '蓝色',
        };
    }
}

echo Color::Red->label(); // '红色'

// 从值获取枚举
$status = Status::from('draft');     // Status::Draft
$status = Status::tryFrom('invalid'); // null(不会抛出异常)

枚举的特性:

  • 枚举可以实现接口
  • 枚举可以使用 trait
  • 枚举不能被实例化(new Status() 是非法的)
  • 枚举 case 是单例

只读属性(Readonly Properties)

属性可以标记为 readonly,只能初始化一次:

class User {
    public readonly string $id;
    public readonly string $name;

    public function __construct(string $id, string $name) {
        $this->id = $id;
        $this->name = $name;
    }
}

$user = new User('123', 'John');
echo $user->id; // '123'

$user->id = '456';
// Fatal error: Cannot modify readonly property User::$id

// 结合构造函数提升使用
class Product {
    public function __construct(
        public readonly string $sku,
        public readonly float $price,
    ) {}
}

只读属性的特性:

  • 只能初始化一次,初始化后不可修改
  • 必须有类型声明
  • 不能有默认值(除非在构造函数中设置)
  • 可以在构造函数或声明时初始化

一等可调用语法(First-class Callable Syntax)

使用 ... 语法创建闭包,替代 Closure::fromCallable()

// 函数转闭包
$strlen = strlen(...);
echo $strlen('hello'); // 5

// 等同于
$strlen = Closure::fromCallable('strlen');

// 方法转闭包
class Calculator {
    public function add(int $a, int $b): int {
        return $a + $b;
    }

    public static function multiply(int $a, int $b): int {
        return $a * $b;
    }

    public function __invoke(int $x): int {
        return $x * 2;
    }
}

$calc = new Calculator();

// 实例方法
$addFn = $calc->add(...);
echo $addFn(3, 5); // 8

// 静态方法
$multiplyFn = Calculator::multiply(...);
echo $multiplyFn(3, 4); // 12

// __invoke 方法
$invokeFn = $calc(...);
echo $invokeFn(10); // 20

// 在数组函数中使用
$words = ['apple', 'banana', 'cherry'];
$lengths = array_map(strlen(...), $words); // [5, 6, 6]

Fibers(纤程)

Fibers 是轻量级的协程,允许暂停和恢复代码执行:

$fiber = new Fiber(function(): void {
    echo "1. Fiber 开始\n";
    $value = Fiber::suspend('暂停值');
    echo "3. Fiber 恢复,收到: $value\n";
});

echo "0. 主程序开始\n";
$suspended = $fiber->start();  // 启动 Fiber
echo "2. 主程序收到: $suspended\n";
$fiber->resume('恢复值');      // 恢复 Fiber
echo "4. 主程序结束\n";

// 输出:
// 0. 主程序开始
// 1. Fiber 开始
// 2. 主程序收到: 暂停值
// 3. Fiber 恢复,收到: 恢复值
// 4. 主程序结束

Fibers 主要用于异步框架(如 ReactPHP、Amp),普通应用开发较少直接使用。

交叉类型(Intersection Types)

使用 & 运算符要求参数同时满足多个类型:

// 参数必须同时实现 Iterator 和 Countable
function processItems(Iterator&Countable $items): int {
    foreach ($items as $item) {
        // 处理项目
    }
    return count($items);
}

// ArrayIterator 同时实现了这两个接口
$items = new ArrayIterator([1, 2, 3]);
echo processItems($items); // 3

// 更复杂的示例
interface Loggable {
    public function log(): void;
}

interface Serializable {
    public function serialize(): string;
}

function store(Loggable&Serializable $object): void {
    $object->log();
    $data = $object->serialize();
    // 存储数据
}

never 返回类型

表示函数永远不会返回(总是抛出异常或终止程序):

function redirect(string $url): never {
    header("Location: $url");
    exit();
}

function throwError(string $message): never {
    throw new RuntimeException($message);
}

// 静态分析工具可以检测 never 之后的死代码
redirect('/home');
echo "这行代码永远不会执行"; // IDE 会标记为死代码

final 类常量

类常量可以标记为 final,防止子类覆盖:

class ParentClass {
    final public const VERSION = '1.0';
    public const NAME = 'Parent';
}

class ChildClass extends ParentClass {
    // ❌ Fatal error: Cannot override final constant
    public const VERSION = '2.0';

    // ✅ 可以覆盖非 final 常量
    public const NAME = 'Child';
}

初始化器中的 new 表达式

可以在默认参数、属性默认值和常量中使用 new 表达式:

// 默认参数
class Logger {
    public function __construct(
        private Formatter $formatter = new DefaultFormatter(),
    ) {}
}

// 属性默认值
class Config {
    public DateTimeImmutable $createdAt = new DateTimeImmutable();
}

// 属性(PHP 8.0 的属性也支持)
#[Attribute]
class Route {
    public function __construct(
        public string $path,
        public array $methods = ['GET'],
    ) {}
}

class Controller {
    #[Route('/users', methods: ['GET', 'POST'])]
    public function users() {}
}

字符串键数组解包

现在可以对字符串键数组使用扩展运算符:

$array1 = ['a' => 1, 'b' => 2];
$array2 = ['c' => 3, 'd' => 4];

// PHP 8.1 之前:只能用于数字键数组
// PHP 8.1:支持字符串键
$merged = [...$array1, ...$array2];
// ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]

// 后面的值会覆盖前面的
$array3 = ['a' => 'override'];
$result = [...$array1, ...$array3];
// ['a' => 'override', 'b' => 2]

显式八进制表示法

新增 0o 前缀表示八进制数:

// 传统八进制表示法(仍然有效)
$permissions = 0755;

// PHP 8.1 新的显式表示法
$permissions = 0o755;

// 两者等价
var_dump(0o16);  // int(14)
var_dump(016);   // int(14)

// 0o 更清晰,避免与数字 0 开头的误解

PHP 8.1 新增函数

array_is_list()

检查数组是否为列表(连续的从 0 开始的整数键):

// 是列表
array_is_list([]);                    // true
array_is_list(['a', 'b', 'c']);       // true
array_is_list([0 => 'a', 1 => 'b']);  // true

// 不是列表
array_is_list([1 => 'a', 2 => 'b']);  // false(不从 0 开始)
array_is_list(['a' => 1, 'b' => 2]);  // false(字符串键)
array_is_list([0 => 'a', 2 => 'b']);  // false(不连续)

fsync() 和 fdatasync()

强制将文件数据写入磁盘:

$file = fopen('data.txt', 'w');
fwrite($file, 'important data');

// fsync: 同步文件数据和元数据
fsync($file);

// fdatasync: 只同步文件数据(更快,不包括元数据)
fdatasync($file);

fclose($file);

Sodium XChaCha20 函数

新增 XChaCha20 加密函数:

// 生成密钥
$key = sodium_crypto_stream_xchacha20_keygen();

// XChaCha20 加密
$nonce = random_bytes(SODIUM_CRYPTO_STREAM_XCHACHA20_NONCEBYTES);
$encrypted = sodium_crypto_stream_xchacha20_xor($message, $nonce, $key);

新的哈希算法

支持 xxHash 和 MurmurHash3:

// xxHash(非常快的非加密哈希)
echo hash('xxh3', 'data');
echo hash('xxh64', 'data');
echo hash('xxh128', 'data');

// MurmurHash3
echo hash('murmur3a', 'data');
echo hash('murmur3c', 'data');
echo hash('murmur3f', 'data');

枚举相关函数

enum Status: string {
    case Active = 'active';
    case Inactive = 'inactive';
}

// 获取所有 case
$cases = Status::cases();
// [Status::Active, Status::Inactive]

// 回退枚举的额外方法
$status = Status::from('active');      // Status::Active
$status = Status::tryFrom('unknown');  // null

弃用特性

向非可空参数传递 null

向内置函数的非可空参数传递 null 已被弃用:

// ❌ PHP 8.1 弃用警告
strlen(null);
str_contains(null, 'test');
trim(null);

// ✅ 修复方法
strlen($value ?? '');
str_contains($value ?? '', 'test');
trim($value ?? '');

Serializable 接口弃用

Serializable 接口已被弃用,使用 __serialize()__unserialize() 替代:

// ❌ 已弃用
class OldWay implements Serializable {
    public function serialize(): string {
        return serialize($this->data);
    }
    public function unserialize(string $data): void {
        $this->data = unserialize($data);
    }
}

// ✅ 推荐方式
class NewWay {
    public function __serialize(): array {
        return ['data' => $this->data];
    }
    public function __unserialize(array $data): void {
        $this->data = $data['data'];
    }
}

隐式浮点数转整数弃用

非整数浮点数隐式转换为整数时会产生弃用警告:

// ❌ 弃用警告
$arr = [];
$arr[3.14] = 'value'; // 3.14 会被转为 3

// ✅ 显式转换
$arr[(int)3.14] = 'value';

日期函数弃用

// ❌ 已弃用
strftime('%Y-%m-%d', time());
gmstrftime('%Y-%m-%d', time());
strptime('2024-01-01', '%Y-%m-%d');
date_sunrise($timestamp);
date_sunset($timestamp);

// ✅ 替代方案
// 使用 IntlDateFormatter
$formatter = new IntlDateFormatter(
    'zh_CN',
    IntlDateFormatter::FULL,
    IntlDateFormatter::NONE
);
echo $formatter->format(time());

// 或使用 DateTime
$date = new DateTime();
echo $date->format('Y-m-d');

mhash*() 函数弃用

// ❌ 已弃用
mhash(MHASH_MD5, 'data');
mhash_keygen_s2k(MHASH_SHA1, 'password', 'salt', 16);

// ✅ 使用 hash() 函数
hash('md5', 'data');
hash_pbkdf2('sha1', 'password', 'salt', 1000, 16);

PDO::FETCH_SERIALIZE 弃用

// ❌ 已弃用
$stmt->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_SERIALIZE, MyClass::class);

// ✅ 使用 __serialize()/__unserialize() 魔术方法

auto_detect_line_endings INI 弃用

// ❌ 已弃用
ini_set('auto_detect_line_endings', true);

// ✅ 手动处理行结束符
$content = str_replace(["\r\n", "\r"], "\n", $content);

兼容性变化

资源类型迁移为对象

多个扩展的资源类型被迁移为对象:

// finfo
$finfo = finfo_open(FILEINFO_MIME);
var_dump($finfo); // PHP 8.0: resource, PHP 8.1: finfo object

// FTP
$ftp = ftp_connect('ftp.example.com');
var_dump($ftp); // FTP\Connection object

// IMAP
$imap = imap_open('{imap.example.com:993/imap/ssl}', 'user', 'pass');
var_dump($imap); // IMAP\Connection object

// LDAP
$ldap = ldap_connect('ldap.example.com');
var_dump($ldap); // LDAP\Connection object

// PostgreSQL
$pg = pg_connect('host=localhost dbname=test');
var_dump($pg); // PgSql\Connection object

继承方法中的静态变量

继承方法中的静态变量现在与父类共享:

class A {
    public static function counter() {
        static $i = 0;
        return ++$i;
    }
}
class B extends A {}

var_dump(A::counter()); // int(1)
var_dump(A::counter()); // int(2)
var_dump(B::counter());
// PHP 8.0: int(1)(B 有自己的计数器)
// PHP 8.1: int(3)(B 共享 A 的计数器)

HTML 实体函数默认处理单引号

// PHP 8.0: 默认 ENT_COMPAT,不转义单引号
// PHP 8.1: 默认 ENT_QUOTES | ENT_SUBSTITUTE

$str = "It's a \"test\"";
echo htmlspecialchars($str);
// PHP 8.0: It's a "test"
// PHP 8.1: It's a "test"

性能改进

PHP 8.1 带来了显著的性能提升:

  • Symfony Demo: 提升约 23%
  • WordPress: 提升约 3.5%
  • 继承缓存优化
  • 快速类名解析
  • 内置函数优化
  • ARM64 JIT 后端支持

总结

PHP 8.1 带来了许多实用的新特性和改进:

主要新特性:

  • 枚举(Enums)
  • 只读属性(Readonly Properties)
  • 一等可调用语法
  • Fibers(纤程)
  • 交叉类型(Intersection Types)
  • never 返回类型
  • final 类常量
  • 初始化器中的 new 表达式
  • 字符串键数组解包

需要注意的变化:

  • 向非可空参数传递 null 已弃用
  • Serializable 接口已弃用
  • 隐式浮点数转整数已弃用
  • 多个资源类型迁移为对象
  • 继承方法中的静态变量行为变化
  • 日期相关函数弃用

通过了解这些变化并遵循迁移建议,可以顺利地从 PHP 8.0 迁移到 PHP 8.1。