PHP 5 → 8:主要特性演进
按版本梳理 PHP 从 5.x 到 8.5 的主要语言特性。每个特性都配有可以直接运行的示例,以及它在真实环境里的输出。
5.x 到 8.3 的示例运行在 PHP 8.3.6 上,8.4 和 8.5 的示例运行在从官方源码编译的 PHP 8.5.0 上。输出没有经过修改,只是把文件路径缩短了。已经不存在于 8.x 的旧行为(比如 PHP 7 里
0 == "abc"为true),会在文字里说明,而不是伪造输出。
总览
| 版本 | 年份 | 代表性特性 |
|---|---|---|
| 5.0 | 2004 | 全新的面向对象模型、异常处理 |
| 5.1 | 2005 | PDO 统一数据库访问层 |
| 5.3 | 2009 | 命名空间、闭包、后期静态绑定 |
| 5.4 | 2012 | Trait、短数组语法 [] |
| 5.5 | 2013 | 生成器 yield |
| 5.6 | 2014 | 可变参数 ...、幂运算符 ** |
| 7.0 | 2015 | Zend Engine 3(性能大幅提升)、标量类型声明、??、<=>、可捕获的 Error |
| 7.1 | 2016 | 可空类型、void、多异常捕获 |
| 7.4 | 2019 | 类型化属性、箭头函数、预加载 |
| 8.0 | 2020 | JIT、命名参数、构造器属性提升、联合类型、match、Nullsafe、Attributes |
| 8.1 | 2021 | 枚举、readonly 属性、Fibers |
| 8.2 | 2022 | readonly 类、DNF 类型 |
| 8.3 | 2023 | 类常量类型、#[\Override]、json_validate() |
| 8.4 | 2024 | 属性钩子、非对称可见性、延迟对象 |
| 8.5 | 2025 | 管道运算符 |>、URI 扩展、clone with、#[\NoDiscard] |
7.2、7.3 等以修补和细节优化为主的版本没有单独列出。
PHP 5.x:现代 PHP 的地基
5.x 之前的 PHP 更像一门“能嵌进 HTML 的脚本语言”。这个系列补齐了工程化开发所需要的东西:像样的面向对象、统一的数据库层、命名空间和代码复用机制。
5.0 全新的面向对象模型
PHP 4 里对象在赋值时会被整体复制,也没有访问控制。5.0 换上了 Zend Engine 2,带来 public / protected / private、抽象类、接口、__construct / __destruct、clone,以及 try / catch 异常。对象变量里保存的是“句柄”,赋值只是多了一个指向同一个对象的引用。
oop-basics.php
<?php
interface Shape
{
public function area();
}
abstract class Base implements Shape
{
protected $name; // 访问控制:protected
public function __construct($name) // 统一的构造函数名
{
$this->name = $name;
}
public function describe()
{
return sprintf("%s 面积 = %.2f", $this->name, $this->area());
}
}
class Circle extends Base
{
private $r;
public function __construct($r)
{
if ($r <= 0) {
throw new InvalidArgumentException("半径必须大于 0");
}
parent::__construct('圆');
$this->r = $r;
}
public function area()
{
return M_PI * $this->r * $this->r;
}
}
$c = new Circle(2);
echo $c->describe(), "\n";
try {
new Circle(-1);
} catch (InvalidArgumentException $e) {
echo "捕获异常: ", $e->getMessage(), "\n";
}
// clone:对象默认按句柄传递,要复制必须显式 clone
$a = new Circle(1);
$b = $a;
$d = clone $a;
var_dump($a === $b, $a === $d);
输出:
$ php oop-basics.php
圆 面积 = 12.57
捕获异常: 半径必须大于 0
bool(true)
bool(false)
看输出的最后两行:$b = $a 只是复制了句柄,所以 $a === $b 为 true;想要真正的副本必须显式 clone,这时 $a === $d 为 false。
5.1 PDO:统一的数据库访问层
在此之前,每种数据库都有自己的一套函数(mysql_query()、pg_query() ……),很多代码靠拼接字符串来构造 SQL。PDO 从 5.1 起随 PHP 一起发布,给所有数据库提供同一套接口,并把预处理语句变成了标准做法。
下面的示例用的是内存中的 SQLite,不需要安装任何数据库就能运行:
pdo.php
<?php
$pdo = new PDO('sqlite::memory:', null, null, array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
));
$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');
// 预处理语句:SQL 和数据分开传递
$insert = $pdo->prepare('INSERT INTO users (name, age) VALUES (:name, :age)');
$insert->execute(array('name' => 'Ann', 'age' => 31));
$insert->execute(array('name' => 'Bob', 'age' => 17));
// 恶意输入只会被当成一段普通文本存进去,不会被当成 SQL 执行
$insert->execute(array('name' => "x'); DROP TABLE users; --", 'age' => 40));
$query = $pdo->prepare('SELECT id, name FROM users WHERE age >= :min ORDER BY id');
$query->execute(array('min' => 18));
foreach ($query->fetchAll() as $row) {
echo $row['id'], ': ', $row['name'], "\n";
}
echo '表还在,共 ', $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(), " 行\n";
// 换数据库只需要改 DSN,其余代码不变,例如:
// new PDO('mysql:host=127.0.0.1;dbname=app;charset=utf8mb4', $user, $pass);
// new PDO('pgsql:host=127.0.0.1;dbname=app', $user, $pass);
输出:
$ php pdo.php
1: Ann
3: x'); DROP TABLE users; --
表还在,共 3 行
输出里那条看起来像 SQL 注入的名字,被当成普通文本存了进去,users 表完好无损。这就是预处理语句的意义:SQL 结构和数据是分开传输的。
顺带一提,PHP 7.0 彻底移除了老的 mysql_* 扩展,所以旧项目升级时,这里往往是第一个要改的地方。
5.3 命名空间、闭包与后期静态绑定
这是 5.x 里影响最大的版本之一。
- 命名空间解决类名冲突,也是后来 Composer 自动加载和现代框架生态得以成立的前提。
- 闭包(匿名函数)可以用
use捕获外部变量,写回调不再需要先给函数起名字。 - 后期静态绑定:
self永远指“这段代码写在哪个类里”,static则指“实际是谁在调用”。基类里写工厂方法、ActiveRecord 之类的场景几乎必用。
namespace-closure.php
<?php
namespace App\Model;
class Model
{
public static function makeSelf()
{
return new self(); // self:永远是"代码写在哪个类里"
}
public static function makeStatic()
{
return new static(); // static:后期静态绑定,是"实际被调用的类"
}
public static function table()
{
$parts = explode('\\', get_called_class());
return strtolower(end($parts)) . 's';
}
}
class User extends Model {}
class Order extends Model {}
echo get_class(User::makeSelf()), "\n";
echo get_class(User::makeStatic()), "\n";
echo User::table(), ' / ', Order::table(), "\n";
// 闭包:用 use 捕获外部变量
$rate = 1.09;
$addTax = function ($price) use ($rate) {
return round($price * $rate, 2);
};
print_r(array_map($addTax, [100, 250, 80]));
// 按引用捕获
$count = 0;
$inc = function () use (&$count) {
$count++;
};
$inc(); $inc(); $inc();
echo "count = $count\n";
输出:
$ php namespace-closure.php
App\Model\Model
App\Model\User
users / orders
Array
(
[0] => 109
[1] => 272.5
[2] => 87.2
)
count = 3
前两行输出是 self 与 static 的区别:通过子类 User 调用时,new self() 得到的仍然是基类 Model,new static() 得到的才是 User。
5.4 Trait 与短数组语法
PHP 是单继承。Trait 允许把一组方法“横向”混入多个不相关的类,相当于由编译器代替你完成复制粘贴。5.4 还带来了短数组语法 [](此前只能写 array()),以及内置的开发用 Web 服务器 php -S localhost:8000。
traits.php
<?php
trait Loggable
{
private $logs = []; // 5.4 短数组语法
public function log($msg)
{
$this->logs[] = $msg;
return $this;
}
public function dump()
{
echo implode(' -> ', $this->logs), "\n";
}
}
trait Timestamps
{
public function touch()
{
return '更新于 2026-09-20';
}
}
class OrderService
{
use Loggable, Timestamps; // 横向复用:不需要继承
}
$s = new OrderService();
$s->log('创建订单')->log('扣减库存')->log('发送通知')->dump();
echo $s->touch(), "\n";
print_r(class_uses($s));
输出:
$ php traits.php
创建订单 -> 扣减库存 -> 发送通知
更新于 2026-09-20
Array
(
[Loggable] => Loggable
[Timestamps] => Timestamps
)
5.5 生成器
函数里出现 yield 后,它就变成了一个可以暂停、恢复的迭代器。处理大文件、大结果集时,不需要先把所有数据装进数组,内存占用从“与数据量成正比”变成“基本恒定”。
generators.php
<?php
function readLines($n)
{
for ($i = 1; $i <= $n; $i++) {
echo " [生成第 $i 项]\n";
yield $i => "line-$i";
}
}
echo "--- 惰性求值:生产与消费交替进行 ---\n";
foreach (readLines(3) as $k => $v) {
echo "消费: $k => $v\n";
}
function xrange($a, $b)
{
for ($i = $a; $i <= $b; $i++) {
yield $i;
}
}
echo "\n--- 内存对比:一百万个整数 ---\n";
$base = memory_get_usage();
$arr = range(1, 1000000);
printf("range() 数组占用: %.1f MB\n", (memory_get_usage() - $base) / 1048576);
unset($arr);
$base = memory_get_usage();
$peak = 0;
foreach (xrange(1, 1000000) as $n) {
$peak = max($peak, memory_get_usage() - $base);
}
printf("生成器 占用 : %.3f MB\n", $peak / 1048576);
输出:
$ php generators.php
--- 惰性求值:生产与消费交替进行 ---
[生成第 1 项]
消费: 1 => line-1
[生成第 2 项]
消费: 2 => line-2
[生成第 3 项]
消费: 3 => line-3
--- 内存对比:一百万个整数 ---
range() 数组占用: 16.0 MB
生成器 占用 : 0.001 MB
上半部分的输出显示了惰性求值:每消费一项,才会去生成下一项。下半部分是内存对比:一百万个整数放进数组需要约 16 MB(这是 PHP 8.3 的数字,PHP 5 下的差距更大),用生成器则几乎不占内存。
同一个版本里还有 finally、password_hash() 和内置的 OPcache。
5.6 可变参数与参数展开
以前要靠 func_get_args() 读取可变参数,函数签名里根本看不出来。...$nums 直接写在参数列表里;调用时再用 ...$array 把数组展开成一个个参数。5.6 还引入了 ** 幂运算符。
variadic.php
<?php
function sum(...$nums) // 收集可变参数(取代 func_get_args)
{
return array_sum($nums);
}
echo sum(1, 2, 3, 4), "\n";
$args = [10, 20, 30];
echo sum(...$args), "\n"; // 参数展开
function connect($host, $port, $timeout)
{
return "$host:$port (timeout=$timeout)";
}
echo connect(...['sg.example.com', 443, 5]), "\n";
echo 2 ** 10, "\n"; // 5.6 新增的幂运算符
输出:
$ php variadic.php
10
60
sg.example.com:443 (timeout=5)
1024
PHP 7.x:性能与类型系统的飞跃
7.0 是这些年最重要的一次升级:引擎底层重写带来了性能翻身,同时语言开始具备现代类型系统的雏形。
7.0 Zend Engine 3:不改代码就变快
7.0 重写了引擎的核心数据结构(更紧凑的变量和数组实现)。在官方基准测试中,常见的结果是比 5.6 快约一倍,内存占用也明显下降。这次升级对业务代码几乎没有要求,所以成了 PHP 生态迁移最快的一次。
这一节没有示例:要复现对比,需要同时装着 5.6 和 7.0,这里只转述官方基准的结论,并没有自己实测。
7.0 标量类型、返回类型与新运算符
现在可以给参数和返回值声明 int、string、float、bool。默认情况下 PHP 仍然会做隐式转换("1" 传给 int 参数会变成 1);在文件开头写上 declare(strict_types=1); 后,类型不匹配就直接抛出 TypeError。同一版本还新增了 ?? 空合并运算符、<=> 太空船运算符和匿名类。
scalar-types.php
<?php
declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
echo add(1, 2), "\n";
try {
echo add("1", 2); // strict_types 下不再做隐式转换
} catch (TypeError $e) {
echo "TypeError: ", strstr($e->getMessage(), ', called', true), "\n";
}
// ?? 空合并:等价于 isset($x) ? $x : 默认值
$user = ['name' => 'Alice'];
echo $user['nick'] ?? '匿名', "\n";
echo $user['name'] ?? '匿名', "\n";
// <=> 太空船:返回 -1 / 0 / 1,写排序回调特别方便
echo (1 <=> 2), ' ', (2 <=> 2), ' ', (3 <=> 2), "\n";
$people = [['n' => 'Bob', 'age' => 30], ['n' => 'Amy', 'age' => 25], ['n' => 'Cat', 'age' => 30]];
usort($people, function ($a, $b) {
return [$b['age'], $a['n']] <=> [$a['age'], $b['n']]; // 年龄降序,同龄按姓名升序
});
echo implode(', ', array_map(function ($p) { return "{$p['n']}({$p['age']})"; }, $people)), "\n";
// 匿名类
$logger = new class {
public function log(string $m): void
{
echo "[LOG] $m\n";
}
};
$logger->log('匿名类可以直接实例化');
输出:
$ php scalar-types.php
3
TypeError: add(): Argument #1 ($a) must be of type int, string given
匿名
Alice
-1 0 1
Bob(30), Cat(30), Amy(25)
[LOG] 匿名类可以直接实例化
??相当于“存在且不为 null 就用它,否则用默认值”,而且不会因为键不存在而产生 Notice。<=>返回-1、0、1。示例里用它对数组做逐项比较,一行写出“年龄降序、同龄按姓名升序”的排序规则。
7.0 可捕获的 Error
在 PHP 5 里,调用不存在的函数、对 null 调用方法这类错误会直接让脚本终止,无法处理。7.0 把它们改成了可以捕获的 Error 异常,并让 Error 和 Exception 共同实现 Throwable 接口:
Throwable
├── Exception 原有的异常体系:RuntimeException、InvalidArgumentException ……
└── Error 7.0 新增:引擎级错误
├── TypeError 类型不匹配(ArgumentCountError 是它的子类)
├── ArithmeticError
│ └── DivisionByZeroError
├── ValueError 8.0 新增:参数值不合法
└── UnhandledMatchError 8.0 新增:match 没有匹配项
throwable.php
<?php
// PHP 7 起,大部分致命错误变成可捕获的 Error,
// Error 与 Exception 都实现 Throwable 接口
function tryIt(callable $fn): void
{
try {
$fn();
} catch (Throwable $t) {
echo get_class($t), ': ', $t->getMessage(), "\n";
}
}
tryIt(function () { undefined_function(); });
tryIt(function () { $x = null; $x->foo(); });
tryIt(function () { echo intdiv(1, 0); });
tryIt(function () { throw new RuntimeException('业务异常也照常捕获'); });
输出:
$ php throwable.php
Error: Call to undefined function undefined_function()
Error: Call to a member function foo() on null
DivisionByZeroError: Division by zero
RuntimeException: 业务异常也照常捕获
这对长驻进程(队列消费者、Swoole / RoadRunner 一类的服务)尤其重要:一个请求出错,可以被捕获、记录,而不会拖垮整个进程。
7.1 可空类型、void、多异常捕获与解构
?string 表示“字符串或 null”;void 声明函数不返回值;catch (A | B $e) 让多种异常共用同一段处理;list() 的简写 [] 也支持按键解构。
nullable-void.php
<?php
// 可空类型 ?T
function greet(?string $name): string
{
return 'Hello, ' . ($name ?? 'guest');
}
echo greet('Ann'), "\n";
echo greet(null), "\n";
// void 返回类型
function logMsg(string $m): void
{
echo "LOG: $m\n";
}
logMsg('无返回值');
// 多异常捕获
function risky(int $n)
{
if ($n === 1) throw new InvalidArgumentException('参数错误');
if ($n === 2) throw new RuntimeException('运行时错误');
return 'ok';
}
foreach ([1, 2, 3] as $n) {
try {
echo risky($n), "\n";
} catch (InvalidArgumentException | RuntimeException $e) {
echo get_class($e), ' -> ', $e->getMessage(), "\n";
}
}
// 短 list 语法 + 按键解构
['id' => $id, 'name' => $name] = ['id' => 7, 'name' => 'Bob'];
[$a, [$b, $c]] = [1, [2, 3]];
echo "$id/$name $a-$b-$c\n";
// 遍历时直接解构
foreach ([['id' => 1, 'tag' => 'x'], ['id' => 2, 'tag' => 'y']] as ['id' => $i, 'tag' => $t]) {
echo "$i:$t ";
}
echo "\n";
输出:
$ php nullable-void.php
Hello, Ann
Hello, guest
LOG: 无返回值
InvalidArgumentException -> 参数错误
RuntimeException -> 运行时错误
ok
7/Bob 1-2-3
1:x 2:y
7.4 类型化属性与箭头函数
类型化属性把类型声明从参数、返回值扩展到了类的属性,属性不再是“什么都能塞”的容器。箭头函数 fn($x) => ... 只能包含一个表达式,会自动按值捕获外层变量,写 array_map / array_filter 的回调时省掉了 function () use (...) { return ...; } 的一大串样板。
typed-props.php
<?php
class Point
{
public int $x = 0;
public ?string $label = null;
public array $tags = [];
}
$p = new Point();
$p->x = 5;
$p->x = "7"; // 非严格模式:数字字符串会被转成 int
var_dump($p->x);
try {
$p->x = "abc"; // 无法转换 -> TypeError
} catch (TypeError $e) {
echo $e->getMessage(), "\n";
}
// 箭头函数:自动按值捕获外层变量
$factor = 3;
$triple = fn($x) => $x * $factor;
print_r(array_map($triple, [1, 2, 3]));
$users = [['name' => 'Ann', 'age' => 17], ['name' => 'Bob', 'age' => 30]];
$adults = array_filter($users, fn($u) => $u['age'] >= 18);
echo implode(',', array_column($adults, 'name')), "\n";
输出:
$ php typed-props.php
int(7)
Cannot assign string to property Point::$x of type int
Array
(
[0] => 3
[1] => 6
[2] => 9
)
Bob
注意第一次赋值 "7" 被悄悄转成了 int(7),这是非严格模式的默认行为;"abc" 无法转换,才会抛出 TypeError。如果需要更严格,同样可以用 declare(strict_types=1);。
7.4 ??=、数组展开与预加载
7.4 里还有几个日常会用到的小改动:??=(“为空才赋值”)、数组里的展开运算符 ...、数字字面量的分隔符 1_000_000。
null-assign-spread.php
<?php
// ??= 空合并赋值
$config = ['debug' => false];
$config['timeout'] ??= 30;
$config['timeout'] ??= 60; // 已有值,不会覆盖
print_r($config);
// 数组展开
$a = [1, 2];
$b = [...$a, 3, ...[4, 5]];
echo implode(',', $b), "\n";
// 数字分隔符
$million = 1_000_000;
echo number_format($million), "\n";
输出:
$ php null-assign-spread.php
Array
(
[debug] =>
[timeout] => 30
)
1,2,3,4,5
1,000,000
预加载(Preloading)是一个部署层面的特性:PHP-FPM 启动时把框架核心类一次性编译进共享内存,之后每个请求直接使用,省去重复加载和编译的开销。它只需要配置,没有可展示的输出:
; php.ini
opcache.preload=/var/www/app/preload.php
opcache.preload_user=www-data
<?php
// preload.php:在服务启动时执行一次
require_once __DIR__ . '/vendor/autoload.php';
foreach (glob(__DIR__ . '/src/Core/*.php') as $file) {
opcache_compile_file($file);
}
被预加载的文件在修改后需要重启 PHP-FPM 才会生效,所以通常只预加载稳定的核心代码。
PHP 8.0:又一次大版本升级
8.0 在语法上的改动集中在“写起来更短、读起来更清楚”,同时把很多长期存在的宽松行为收紧了。
8.0 JIT 编译器
JIT 把热点代码直接编译成机器码。它对纯计算型的任务(图像处理、数值运算、算法)提升很大,但一般的 Web 应用大部分时间花在数据库和网络 I/O 上,收益有限。启用方式是在 php.ini 里配置:
opcache.enable=1
opcache.jit_buffer_size=100M
opcache.jit=tracing
下面用一个纯计算的曼德博集合来对比。脚本会跑 3 次,取最快的一次:
mandelbrot.php
<?php
// CPU 密集型:曼德博集合(纯数值计算,几乎没有 I/O)
function mandelbrot(int $size): int
{
$inside = 0;
for ($y = 0; $y < $size; $y++) {
for ($x = 0; $x < $size; $x++) {
$cr = 2.0 * $x / $size - 1.5;
$ci = 2.0 * $y / $size - 1.0;
$zr = $zi = 0.0;
$i = 0;
while ($i < 100 && $zr * $zr + $zi * $zi < 4.0) {
$t = $zr * $zr - $zi * $zi + $cr;
$zi = 2.0 * $zr * $zi + $ci;
$zr = $t;
$i++;
}
if ($i === 100) {
$inside++;
}
}
}
return $inside;
}
// 跑 3 次,取最快的一次
$best = INF;
for ($round = 0; $round < 3; $round++) {
$start = microtime(true);
$n = mandelbrot(500);
$best = min($best, microtime(true) - $start);
}
printf("集合内点数=%d,最快一次耗时 %.2f 秒\n", $n, $best);
输出:
$ php -d opcache.enable_cli=1 -d opcache.jit=off mandelbrot.php
集合内点数=96625,最快一次耗时 0.18 秒
输出:
$ php -d opcache.enable_cli=1 -d opcache.jit_buffer_size=64M -d opcache.jit=tracing mandelbrot.php
集合内点数=96625,最快一次耗时 0.04 秒
具体的耗时取决于机器,请把注意力放在两次结果的倍数上:这里快了 4 倍以上。这类几乎没有 I/O 的紧凑循环是 JIT 最擅长的场景;换成典型的“查库、渲染模板”的请求,差距会小得多。
8.0 命名参数、构造器属性提升与联合类型
- 命名参数:调用时写出参数名,可以跳过中间的可选参数,也让
true、false这类含义不明的实参一目了然。 - 构造器属性提升:在构造函数的参数前直接写可见性,声明属性和赋值一步完成,值对象和 DTO 的样板代码少了一大半。
- 联合类型:
int|float|string,把“可以是几种类型之一”写进签名。 throw表达式:throw现在可以出现在??、箭头函数等表达式的位置。
named-args.php
<?php
// 命名参数:跳过中间的可选参数,调用处一目了然
function createUser(string $name, int $age = 18, bool $admin = false, string $lang = 'zh')
{
return sprintf("%s, %d岁, admin=%s, lang=%s", $name, $age, var_export($admin, true), $lang);
}
echo createUser('Tom', admin: true), "\n";
echo createUser(name: 'Amy', lang: 'en', age: 22), "\n";
// 构造器属性提升:声明 + 赋值一步完成
class User
{
public function __construct(
public string $name,
private int $age = 0,
protected array $roles = [],
) {}
public function age(): int
{
return $this->age;
}
}
$u = new User('Ann', 30, ['editor']);
print_r($u);
echo $u->name, ' / ', $u->age(), "\n";
// 联合类型
function format(int|float|string $v): string
{
return gettype($v) . ':' . $v;
}
echo format(42), ' ', format(3.14), ' ', format('hi'), "\n";
// throw 变成表达式
$input = ['id' => 1];
try {
$name = $input['name'] ?? throw new InvalidArgumentException('缺少 name');
} catch (InvalidArgumentException $e) {
echo $e->getMessage(), "\n";
}
输出:
$ php named-args.php
Tom, 18岁, admin=true, lang=zh
Amy, 22岁, admin=false, lang=en
User Object
(
[name] => Ann
[age:User:private] => 30
[roles:protected] => Array
(
[0] => editor
)
)
Ann / 30
integer:42 double:3.14 string:hi
缺少 name
print_r 的输出可以看到,private 和 protected 的构造器参数已经变成了类的属性,没有写任何一行 $this->x = $x;。
8.0 match 表达式
match 可以看作更严格的 switch:它是表达式,有返回值;使用严格比较(===);不需要 break;没有任何分支匹配时会抛出 UnhandledMatchError,而不是静默略过。
match.php
<?php
$code = 404;
// match 是表达式,有返回值,使用严格比较(===),不需要 break
$msg = match ($code) {
200, 201 => '成功',
301, 302 => '重定向',
404 => '未找到',
500 => '服务器错误',
default => '未知',
};
echo "$code => $msg\n";
// match(true) 可以替代 if-elseif 链
$score = 85;
echo match (true) {
$score >= 90 => 'A',
$score >= 80 => 'B',
$score >= 60 => 'C',
default => 'D',
}, "\n";
// 没有匹配项且没有 default:抛出 UnhandledMatchError(switch 会静默略过)
try {
echo match (999) {
1 => 'one',
};
} catch (\UnhandledMatchError $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
// 与 switch 的宽松比较对照
$v = "0";
switch ($v) {
case 0: echo "switch: 匹配 case 0(宽松 ==)\n"; break;
default: echo "switch: default\n";
}
echo 'match : ', match ($v) { 0 => '匹配 0', default => '严格比较,"0" !== 0,走 default' }, "\n";
输出:
$ php match.php
404 => 未找到
B
UnhandledMatchError: Unhandled match case 999
switch: 匹配 case 0(宽松 ==)
match : 严格比较,"0" !== 0,走 default
最后两行展示了它和 switch 的关键差别:"0" 与 0 在 switch 的宽松比较下相等,在 match 的严格比较下不相等。
8.0 Nullsafe 运算符与 Attributes
Nullsafe 运算符 ?->:链条上任何一环是 null,整条表达式直接得到 null,不用再写一层层 if。
Attributes(注解) #[...]:给类、方法、参数附加结构化的元数据,并且能通过反射读取。此前只能靠解析 docblock 注释,框架里的路由、校验、ORM 映射现在可以直接使用它。
nullsafe-attributes.php
<?php
class Address { public function __construct(public ?string $city = null) {} }
class Profile { public function __construct(public ?Address $address = null) {} }
class Account
{
public function __construct(private ?Profile $profile = null) {}
public function getProfile(): ?Profile { return $this->profile; }
}
$a1 = new Account(new Profile(new Address('Singapore')));
$a2 = new Account(new Profile()); // 没有地址
$a3 = null; // 没有账户
// Nullsafe:链上任何一环为 null,整条链直接返回 null
foreach ([$a1, $a2, $a3] as $acc) {
echo $acc?->getProfile()?->address?->city ?? '(无)', "\n";
}
echo "\n--- Attributes(注解)---\n";
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(public string $path, public string $method = 'GET') {}
}
class HomeController
{
#[Route('/home')]
public function index() {}
#[Route('/users', method: 'POST')]
public function store() {}
}
foreach ((new ReflectionClass(HomeController::class))->getMethods() as $m) {
foreach ($m->getAttributes(Route::class) as $attr) {
$r = $attr->newInstance();
printf("%-4s %-8s -> %s::%s()\n", $r->method, $r->path, $m->class, $m->name);
}
}
输出:
$ php nullsafe-attributes.php
Singapore
(无)
(无)
--- Attributes(注解)---
GET /home -> HomeController::index()
POST /users -> HomeController::store()
8.0 更严格的类型与比较
8.0 修正了一批历史遗留的宽松行为,其中影响最大的是字符串与数字的比较:过去 0 == "abc" 会先把 "abc" 转成数字 0,结果为 true;现在只有当字符串本身是数字字符串时才按数字比较,否则把数字转成字符串再比较。另外,对非数字字符串做算术运算,以及给内部函数传错类型,都会抛出异常,而不是悄悄继续执行。
string-compare.php
<?php
// 新的字符串函数
var_dump(str_contains('Hello World', 'World'));
var_dump(str_starts_with('Hello World', 'Hello'));
var_dump(str_ends_with('Hello World', 'World'));
// 字符串与数字的比较规则变了(PHP 7 里下面这些结果是 true)
var_dump(0 == "abc");
var_dump(0 == "");
var_dump("abc" == 0);
var_dump(0 == "0"); // 数字字符串仍然按数字比较
var_dump(100 == "1e2");
// 对非数字字符串做算术运算:抛异常,而不是悄悄当成 0
try {
echo "abc" * 2;
} catch (TypeError $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
// 内部函数传入错误类型:抛 TypeError,而不是 Warning + 返回 null
try {
strlen([]);
} catch (TypeError $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
输出:
$ php string-compare.php
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)
bool(false)
bool(true)
bool(true)
TypeError: Unsupported operand types: string * int
TypeError: strlen(): Argument #1 ($string) must be of type string, array given
在 PHP 7 里,0 == "abc"、0 == ""、"abc" == 0 这三个比较的结果都是 true。"abc" * 2 会悄悄得到 0(7.1 起附带一条 Warning),strlen([]) 则是 Warning 加返回 null。老代码里如果依赖了这些行为,升级后会直接暴露出来。
同一批新增的 str_contains()、str_starts_with()、str_ends_with(),终于让“判断字符串包含/开头/结尾”不必再写 strpos(...) !== false。
PHP 8.1 ~ 8.5:8.x 系列的持续增强
从 8.1 开始,每个版本都在补齐现代静态类型语言的能力:枚举、不可变对象、属性钩子……
8.1 枚举
枚举是一种受限的类:它的取值只能是预先声明的几个实例。它可以带“底层值”(string 或 int),可以有方法、常量,可以实现接口,也能直接作为类型约束。以前常见的做法是“一堆类常量加一段校验代码”,现在语言层面就保证了取值合法。
enum.php
<?php
enum Suit: string
{
case Hearts = 'H';
case Spades = 'S';
case Clubs = 'C';
public function label(): string
{
return match ($this) {
self::Hearts => '红桃',
self::Spades => '黑桃',
self::Clubs => '梅花',
};
}
public function color(): string
{
return $this === self::Hearts ? '红' : '黑';
}
const Wild = self::Spades;
}
echo Suit::Hearts->name, ' = ', Suit::Hearts->value, "\n";
echo Suit::from('S')->label(), "\n"; // 值 -> 枚举,找不到会抛异常
echo Suit::tryFrom('X')?->label() ?? '(tryFrom 找不到,返回 null)', "\n";
try {
Suit::from('X');
} catch (ValueError $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
echo implode(', ', array_map(fn(Suit $s) => $s->label(), Suit::cases())), "\n";
function play(Suit $s): string // 枚举可以直接作为类型约束
{
return "打出 {$s->label()}({$s->color()})";
}
echo play(Suit::Clubs), "\n";
var_dump(Suit::Hearts instanceof Suit, Suit::Wild === Suit::Spades);
输出:
$ php enum.php
Hearts = H
黑桃
(tryFrom 找不到,返回 null)
ValueError: "X" is not a valid backing value for enum Suit
红桃, 黑桃, 梅花
打出 梅花(黑)
bool(true)
bool(true)
from()找不到时抛出ValueError,tryFrom()则返回null。cases()返回所有枚举成员,适合用来生成下拉选项。- 枚举成员是单例,所以可以直接用
===比较。
8.1 readonly 属性
readonly 属性只能初始化一次(并且只能在类的内部),之后任何修改都会抛出 Error。用它来写值对象(金额、坐标、配置)最合适:对象一旦创建,就不会在别处被意外改掉。
readonly.php
<?php
final class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {}
// 不可变对象:修改 = 返回一个新对象
public function add(int $n): static
{
return new static($this->amount + $n, $this->currency);
}
}
$m1 = new Money(100, 'SGD');
$m2 = $m1->add(50);
echo "{$m1->amount} {$m1->currency} -> {$m2->amount} {$m2->currency}\n";
try {
$m1->amount = 999;
} catch (Error $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
输出:
$ php readonly.php
100 SGD -> 150 SGD
Error: Cannot modify readonly property Money::$amount
8.1 Fibers
Fiber 是底层的协程原语:一段代码可以在中途“挂起”,把控制权交还给调用者,之后再从原来的位置继续。它本身不提供异步 I/O,而是给 Revolt、ReactPHP、Amp 这类事件循环库打下基础。日常业务代码很少直接使用它。
fiber.php
<?php
$fiber = new Fiber(function (string $name): string {
echo "[$name] 开始\n";
$x = Fiber::suspend('第 1 次暂停'); // 在这里挂起,把值交给外层
echo "[$name] 收到: $x\n";
$y = Fiber::suspend('第 2 次暂停');
echo "[$name] 收到: $y\n";
return "$name 完成";
});
$v = $fiber->start('任务A');
echo "主流程拿到: $v\n";
$v = $fiber->resume('数据1');
echo "主流程拿到: $v\n";
$fiber->resume('数据2');
echo "主流程拿到返回值: ", $fiber->getReturn(), "\n";
var_dump($fiber->isTerminated());
// 交替调度两个纤程(协程调度器的雏形)
echo "\n--- 简易调度 ---\n";
$tasks = [];
foreach (['A', 'B'] as $id) {
$tasks[$id] = new Fiber(function () use ($id) {
for ($i = 1; $i <= 2; $i++) {
echo "任务{$id} 第{$i} 步\n";
Fiber::suspend();
}
});
$tasks[$id]->start();
}
while ($tasks) {
foreach ($tasks as $id => $f) {
$f->resume();
if ($f->isTerminated()) unset($tasks[$id]);
}
}
输出:
$ php fiber.php
[任务A] 开始
主流程拿到: 第 1 次暂停
[任务A] 收到: 数据1
主流程拿到: 第 2 次暂停
[任务A] 收到: 数据2
主流程拿到返回值: 任务A 完成
bool(true)
--- 简易调度 ---
任务A 第1 步
任务B 第1 步
任务A 第2 步
任务B 第2 步
前半部分展示了挂起与恢复时如何双向传值;后半部分用最简单的轮转调度让两个任务交替运行,这就是协程调度器的雏形。
8.1 其他:可调用语法、never 与 new in initializers
strlen(...)、$obj->method(...)这种 一等公民可调用语法,是Closure::fromCallable()的简写。never返回类型:函数不会正常返回(一定抛异常或exit)。new可以出现在参数默认值里,依赖注入时可以直接给出默认实现。
callable-never.php
<?php
// 一等公民可调用语法:用 (...) 直接创建闭包
$len = strlen(...);
echo $len('hello'), "\n";
class Formatter
{
public function upper(string $s): string { return strtoupper($s); }
public static function wrap(string $s): string { return "[$s]"; }
}
$f = new Formatter();
$upper = $f->upper(...);
$wrap = Formatter::wrap(...);
echo implode(' ', array_map($wrap, array_map($upper, ['a', 'b']))), "\n";
// never:函数永远不会正常返回
function fail(string $msg): never
{
throw new RuntimeException($msg);
}
try {
fail('走到这里就一定是异常');
} catch (RuntimeException $e) {
echo $e->getMessage(), "\n";
}
// new 可以出现在参数默认值里
class Logger { public function __construct(public string $ch = 'app') {} }
class Service
{
public function __construct(private Logger $logger = new Logger('default')) {}
public function channel(): string { return $this->logger->ch; }
}
echo (new Service())->channel(), "\n";
// array_is_list
var_dump(array_is_list([1, 2, 3]), array_is_list([1 => 'a', 2 => 'b']));
输出:
$ php callable-never.php
5
[A] [B]
走到这里就一定是异常
default
bool(true)
bool(false)
8.2 readonly 类、DNF 类型与动态属性弃用
readonly类:在类名前写一次readonly,所有属性自动变成只读。- DNF 类型:允许把交集类型放进联合类型,写成
(A&B)|null这样。 - 动态属性被弃用:给没有声明过的属性赋值(
$obj->foo = 1)现在会触发 Deprecated,这是 8.x 升级中很常见的一条提示。
readonly-class.php
<?php
// readonly 类:所有属性自动变成 readonly
readonly class Point
{
public function __construct(public int $x, public int $y) {}
public function withX(int $x): static
{
return new static($x, $this->y);
}
}
$p = new Point(1, 2);
$q = $p->withX(10);
echo "($p->x, $p->y) -> ($q->x, $q->y)\n";
try {
$p->y = 5;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
// DNF 类型:交集类型可以放进联合类型
interface HasId { public function id(): int; }
interface HasName { public function name(): string; }
class Product implements HasId, HasName
{
public function id(): int { return 7; }
public function name(): string { return '键盘'; }
}
function describe((HasId&HasName)|null $e): string
{
return $e === null ? '(空)' : "#{$e->id()} {$e->name()}";
}
echo describe(new Product()), ' | ', describe(null), "\n";
// 独立的 true / false / null 类型
function ok(): true { return true; }
var_dump(ok());
// 动态属性在 8.2 被弃用(8.3 仍会提示)
class Legacy {}
$o = new Legacy();
$o->foo = 1;
echo "foo = {$o->foo}\n";
输出:
$ php readonly-class.php
(1, 2) -> (10, 2)
Cannot modify readonly property Point::$y
#7 键盘 | (空)
bool(true)
Deprecated: Creation of dynamic property Legacy::$foo is deprecated in readonly-class.php on line 45
foo = 1
最后的 Deprecated 是在 8.2 起新增的提示:程序仍然能运行,但这种写法未来的版本会禁止。如果类确实需要动态属性,可以给它加上 #[\AllowDynamicProperties];更好的做法是把属性正式声明出来。
8.3 类常量类型、#[\Override] 与 json_validate()
- 类常量可以声明类型,
const string NAME = ...;。 #[\Override]表示“这个方法必须覆盖父类或接口里的方法”。方法名拼错或父类改名之后,PHP 会在类加载阶段直接报致命错误,而不是悄悄变成一个新方法。json_validate()只判断字符串是不是合法 JSON,不用真的解码出一大棵数据。
typed-const.php
<?php
// 类常量可以声明类型
class App
{
public const string NAME = 'demo';
public const int MAX_RETRY = 3;
}
echo App::NAME, ' x', App::MAX_RETRY, "\n";
// 动态类常量读取:Foo::{$name}
$key = 'MAX_RETRY';
echo App::{$key}, "\n";
// json_validate:只校验,不用真的解码整个结构
var_dump(json_validate('{"name":"Ann","tags":[1,2,3]}'));
var_dump(json_validate('{name:"Ann"}'));
echo json_last_error_msg(), "\n";
输出:
$ php typed-const.php
demo x3
3
bool(true)
bool(false)
Syntax error
下面是 #[\Override] 拦截拼写错误的例子:helo() 在父类里并不存在。
override.php
<?php
class ParentClass
{
public function hello(): string { return 'P'; }
}
class ChildClass extends ParentClass
{
#[\Override]
public function helo(): string // 手滑写错了方法名
{
return 'C';
}
}
echo (new ChildClass())->helo();
输出:
$ php override.php
Fatal error: ChildClass::helo() has #[\Override] attribute, but no matching parent method exists in override.php on line 10
8.4 属性钩子
属性钩子(Property Hooks)让属性自己带上 get / set 逻辑,不必再为了一点校验或计算,专门写一对 getter / setter 方法。有底层存储的属性用 set 做校验和规范化;没有底层存储的“虚拟属性”只写 get,读取时才计算。
property-hooks.php
<?php
class User
{
public function __construct(
public string $first,
public string $last,
) {}
// 虚拟属性:没有底层存储,读取时才计算
public string $fullName {
get => $this->first . ' ' . $this->last;
}
// set 钩子:赋值时校验 + 规范化
public string $email {
set(string $value) {
if (!str_contains($value, '@')) {
throw new InvalidArgumentException("邮箱格式不对: $value");
}
$this->email = strtolower($value);
}
}
// 短语法:set => 表达式(参数名固定为 $value)
public string $nickname = '' {
set => trim($value);
}
}
$u = new User('Ada', 'Lovelace');
echo $u->fullName, "\n";
$u->email = 'ADA@Example.COM';
echo $u->email, "\n";
$u->nickname = ' ada ';
var_dump($u->nickname);
try {
$u->email = 'oops';
} catch (InvalidArgumentException $e) {
echo $e->getMessage(), "\n";
}
try {
$u->fullName = 'X Y'; // 只有 get 钩子的属性不能被写入
} catch (Error $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
输出:
$ php property-hooks.php
Ada Lovelace
ada@example.com
string(3) "ada"
邮箱格式不对: oops
Error: Property User::$fullName is read-only
8.4 非对称可见性
读和写可以有不同的可见性:public private(set) 表示“任何人可读,只有类内部可写”。以前要达到这个效果,得写 private 属性加一个 public getter,现在一个声明就行。
asymmetric.php
<?php
class Account
{
public function __construct(
public private(set) int $balance = 0, // 任何人可读,只有类内部可写
public protected(set) string $owner = 'n/a', // 子类也可写
) {}
public function deposit(int $amount): void
{
$this->balance += $amount;
}
}
$a = new Account(100, 'Ann');
$a->deposit(50);
echo "余额: {$a->balance}, 户主: {$a->owner}\n";
try {
$a->balance = 999999; // 外部直接改:不允许
} catch (Error $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
输出:
$ php asymmetric.php
余额: 150, 户主: Ann
Error: Cannot modify private(set) property Account::$balance from global scope
8.4 延迟对象
延迟对象(Lazy Objects)允许创建一个“占位”对象,直到第一次真正访问它的属性或方法时才执行构造逻辑。ORM 的懒加载、依赖注入容器里“用到才创建”的服务,以前要靠生成代理类实现,现在语言原生支持。
lazy.php
<?php
class HeavyService
{
public string $name;
public function __construct()
{
echo " [HeavyService 构造:假装在连接数据库…]\n";
$this->name = 'db-connection';
}
public function query(): string
{
return "来自 {$this->name} 的查询结果";
}
}
$ref = new ReflectionClass(HeavyService::class);
// 创建"延迟幽灵对象":此刻不会执行构造函数
$svc = $ref->newLazyGhost(function (HeavyService $obj): void {
$obj->__construct();
});
echo "对象已创建,尚未初始化: ";
var_dump($ref->isUninitializedLazyObject($svc));
echo "第一次访问 ->\n";
echo $svc->query(), "\n";
echo "现在已初始化: ";
var_dump(!$ref->isUninitializedLazyObject($svc));
输出:
$ php lazy.php
对象已创建,尚未初始化: bool(true)
第一次访问 ->
[HeavyService 构造:假装在连接数据库…]
来自 db-connection 的查询结果
现在已初始化: bool(true)
输出里 HeavyService 的构造函数直到调用 query() 时才执行;在此之前对象已经存在,只是尚未初始化。
8.4 其他
new Foo()->method()不必再给new表达式包一层括号。- 新增
array_find()、array_find_key()、array_any()、array_all()。 #[\Deprecated]让你自己写的函数和方法也能触发标准的弃用提示。
array-find.php
<?php
// new 表达式后可以直接链式调用,不用再包一层括号
echo new DateTimeImmutable('2026-09-20')->format('Y-m-d (l)'), "\n";
// 数组查找函数:array_find / array_find_key / array_any / array_all
$nums = [3, 8, 12, 5];
var_dump(array_find($nums, fn($n) => $n > 5));
var_dump(array_find_key($nums, fn($n) => $n > 5));
var_dump(array_any($nums, fn($n) => $n % 2 === 0));
var_dump(array_all($nums, fn($n) => $n > 0));
// #[\Deprecated]:自己写的函数也能触发标准的弃用提示
#[\Deprecated('请改用 newApi()', since: '1.2')]
function oldApi(): string
{
return 'old';
}
echo oldApi(), "\n";
输出:
$ php array-find.php
2026-09-20 (Sunday)
int(8)
int(1)
bool(true)
bool(true)
Deprecated: Function oldApi() is deprecated since 1.2, 请改用 newApi() in array-find.php on line 18
old
同时,8.4 起弃用了“类型加 = null”的隐式可空写法。升级到 8.4 之后,老代码里常会看到这条提示:
nullable-deprecated.php
<?php
// "类型 + 默认值 null" 的隐式可空写法,在 8.4 起被弃用
function greet(string $name = null): string
{
return 'Hello, ' . ($name ?? 'guest');
}
echo greet(), "\n";
输出:
$ php nullable-deprecated.php
Deprecated: greet(): Implicitly marking parameter $name as nullable is deprecated, the explicit nullable type must be used instead in nullable-deprecated.php on line 3
Hello, guest
修复方法很简单:写成显式的 ?string $name = null。
8.5 管道运算符
|> 把左边的值作为唯一参数传给右边的可调用对象,让“一连串函数依次处理同一个值”的代码可以从左到右读。右边可以是函数、strlen(...) 这种一等公民可调用语法,或者用括号包起来的箭头函数。
pipe.php
<?php
// 没有管道运算符:由内向外读,嵌套很深
$slug1 = str_replace(' ', '-', strtolower(trim(' Hello PHP World ')));
// 有了 |>:从左到右,像流水线
$slug2 = ' Hello PHP World '
|> trim(...)
|> strtolower(...)
|> (fn(string $s) => str_replace(' ', '-', $s));
var_dump($slug1 === $slug2);
echo $slug2, "\n";
// 处理数组:过滤偶数再求和
$total = [1, 2, 3, 4, 5, 6]
|> (fn(array $a) => array_filter($a, fn($n) => $n % 2 === 0))
|> array_sum(...);
echo $total, "\n";
输出:
$ php pipe.php
bool(true)
hello-php-world
12
8.5 URI 扩展
内置的 URI 扩展按 RFC 3986 和 WHATWG 两套标准解析 URL,返回不可变对象;解析失败时抛出明确的异常,可以替代行为不够严谨的 parse_url()。下面的示例用的是 RFC 3986 版本 Uri\Rfc3986\Uri。
uri.php
<?php
use Uri\Rfc3986\Uri;
$uri = new Uri('https://user@Example.COM:8080/docs/php/../guide?lang=zh&v=8.5#intro');
echo 'scheme : ', $uri->getScheme(), "\n";
echo 'host : ', $uri->getHost(), "\n";
echo 'port : ', $uri->getPort(), "\n";
echo 'path : ', $uri->getPath(), "\n";
echo 'query : ', $uri->getQuery(), "\n";
echo 'fragment: ', $uri->getFragment(), "\n";
// getXxx() 返回规范化之后的值,getRawXxx() 返回原文
echo "\n";
echo 'host (规范化): ', $uri->getHost(), "\n";
echo 'host (原文) : ', $uri->getRawHost(), "\n";
echo 'path (规范化): ', $uri->getPath(), "\n";
echo 'path (原文) : ', $uri->getRawPath(), "\n";
// 不可变对象:with*() 返回新对象,原对象不变
echo "\n";
$next = $uri->withHost('example.org')->withPath('/new')->withQuery(null)->withFragment(null);
echo $next->toString(), "\n";
echo $uri->toString(), "\n";
// 非法 URI 会抛出明确的异常
echo "\n";
try {
new Uri('https://exa mple.com');
} catch (\Uri\InvalidUriException $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
输出:
$ php uri.php
scheme : https
host : example.com
port : 8080
path : /docs/guide
query : lang=zh&v=8.5
fragment: intro
host (规范化): example.com
host (原文) : Example.COM
path (规范化): /docs/guide
path (原文) : /docs/php/../guide
https://user@example.org:8080/new
https://user@example.com:8080/docs/guide?lang=zh&v=8.5#intro
Uri\InvalidUriException: The specified URI is malformed
getXxx() 返回规范化之后的值(主机名转成小写、路径里的 .. 被折叠),getRawXxx() 返回原文。withXxx() 不会修改原对象,而是返回一个新对象,所以最后两行能同时看到修改前后的结果。传入非法字符时,会得到 Uri\InvalidUriException,而不是一个含糊的 false。
8.5 clone with
clone 现在可以带第二个参数,一步完成“复制并修改某些属性”。它对 readonly 对象特别有用:以前写“with 方法”要把每个属性手动搬一遍,属性一多就很啰嗦。
clone-with.php
<?php
final class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {}
// 以前:要手写 new static(...) 并把每个属性搬一遍
// 现在:clone 时直接覆盖想改的属性(连 readonly 也可以)
public function withAmount(int $amount): static
{
return clone($this, ['amount' => $amount]);
}
}
$a = new Money(100, 'SGD');
$b = $a->withAmount(250);
echo "{$a->amount} {$a->currency} / {$b->amount} {$b->currency}\n";
输出:
$ php clone-with.php
100 SGD / 250 SGD
8.5 array_first()、array_last() 与 #[\NoDiscard]
array_first() 和 array_last() 取代了 reset() / end() 或 $a[array_key_first($a)] 这类绕弯的写法,空数组时返回 null。
#[\NoDiscard] 标记“返回值不应该被忽略”的函数,比如保存可能失败的操作;调用者忽略返回值时会得到 Warning。确实不需要结果时,可以用 8.5 新增的 (void) 转换明确表示“我是故意的”。
no-discard.php
<?php
// array_first / array_last:不用再 reset()/end() 或者 $a[array_key_first($a)]
$list = ['b' => 2, 'a' => 1, 'c' => 3];
echo array_first($list), ' ', array_last($list), "\n";
var_dump(array_first([]));
// #[\NoDiscard]:返回值不该被忽略的函数
#[\NoDiscard('保存可能失败,必须检查返回值')]
function save(array $data): bool
{
return count($data) > 0;
}
save([]); // 忽略返回值 -> Warning
$ok = save(['x' => 1]); // 使用了返回值 -> 没问题
(void) save([]); // (void) 强制转换:明确表示"我故意不要结果"
echo "结束\n";
输出:
$ php no-discard.php
2 3
NULL
Warning: The return value of function save() should either be used or intentionally ignored by casting it as (void), 保存可能失败,必须检查返回值 in no-discard.php on line 14
结束
升级时最容易踩的坑
从 5.x 直接升到 8.x,大部分问题都出在“被移除的东西”和“被收紧的宽松行为”上。
| 变化 | 版本 | 处理办法 |
|---|---|---|
mysql_* 系列函数被移除 |
7.0 | 改用 PDO 或 mysqli |
ereg* / split() 被移除 |
7.0 | 改用 preg_* |
each()、create_function() 被移除 |
8.0 | 改用 foreach 与匿名函数 |
$str{0} 花括号取字符被移除 |
8.0 | 改成 $str[0] |
| 字符串与数字的比较规则改变 | 8.0 | 重新检查所有 ==,需要严格比较时用 === |
内部函数传错类型抛出 TypeError |
8.0 | 修正调用处的类型,别依赖静默转换 |
| 动态属性被弃用 | 8.2 | 声明属性,或加 #[\AllowDynamicProperties] |
"${var}" 字符串插值被弃用 |
8.2 | 改成 "{$var}" 或 "$var" |
| 隐式可空参数被弃用 | 8.4 | 写成 ?T $x = null |
一个实用的升级顺序是:先把错误报告调到 E_ALL,在目标版本上跑一遍测试,把所有 Deprecated 提示清掉,再去用新特性。Deprecated 是提前通知:被弃用的写法,在之后的版本里通常会变成真正的错误。