简介:
自 PHP 8.0 发布以来,该语言经历了一系列重要的功能迭代与性能改进。对于开发者而言,系统理解这些新特性不仅是技术更新的需要,更是优化代码设计、降低维护成本的有效途径。本分类将按版本梳理 PHP 8.x 的新特性。
8.4 官方手册参考指南(含其他特性):官方链接
一、属性钩子
属性钩子是 Zend 引擎原生支持的属性访问拦截机制,在属性读取(get)、写入(set)时自动触发自定义逻辑,无需额外方法调用。
告别冗余,拥抱原生。属性钩子的优势是颠覆性的:
- 原生性能:它是 Zend 引擎在语言层面直接支持的机制,性能远优于
__get/__set魔术方法 - 更优雅的语法:告别重复的方法体,直接在属性上定义存取逻辑,代码更简洁
- 出色的工具支持:IDE 和静态分析工具(如 PHPStan)能原生识别属性钩子,告别了依赖
@property注解的时代 - 精准的控制:与"一揽子"拦截的
__get/__set不同,钩子可以精准地为单个属性定制行为,同时为未来预留扩展可能,避免了 API 的不兼容变更
语法示例:
php
<?php
declare(strict_types=1);
class Example
{
public string $firstName;
public string $lastName;
// 使用箭头函数定义只读的计算属性 仅定义get
public string $fullName
{
get => $this->firstName .' '. $this->lastName;
}
// 使用代码块定义带验证的 setter 仅定义set
public string $email
{
set(string $value){
if(!filter_var($value, FILTER_VALIDATE_EMAIL)){
throw new InvalidArgumentException("Invalid email address");
}
$this->email = strtolower($value);
}
}
}
$example = new Example();
$example->firstName = "John";
$example->lastName = "Doe";
echo $example->fullName; // John Doe
echo '<br/>';
$example->email = "John@Example.COM";
echo $example->email; // john@example.com
使用场景:
- 赋值时数据校验 + 清洗(最核心),写入属性时自动触发
set,无需手动调用方法。例:邮箱、手机号、年龄、价格、用户名等合法性校验;自动去空格、转小写 - 读取时自动格式化(不修改原值),读取属性时自动触发
get,原值保持原始格式。例:姓名大写、日期格式化、金额加货币符号、手机号脱敏 - 虚拟 / 计算属性(无存储,动态派生),每次读取触发
get计算,无存储、不占用内存。例:全名、订单总价、BMI、年龄计算等无需存库的派生字段 - 非对称可见性(对外只读,内部可写),外部写入直接报错,内部写入触发
set。例:ID、创建时间、状态等禁止外部修改的字段 - 类型自动双向转换(ORM / 存储层常用),读→转对象 / 数组,写→转字符串存库。例:数据库存字符串,代码用对象;数组↔JSON 互转
- 属性变更监听(日志、触发事件、联动更新),赋值时 set 执行副作用逻辑。例:记录修改日志、状态变更时更新其他字段、触发通知
- 防非法赋值(自动修正,不抛异常),写入时
set自动修正值。例:库存不能为负、价格默认 0、空值自动填充
二、不对称可见性
在 PHP8.4 之前,类属性的可见性 (public/protected/private) 是对称的------ 读取和写入操作必须遵循相同的访问规则。如果需要实现 "对外只读、对内可写" 的常见需求,开发者只能通过以下两种繁琐方式实现:
- 私有属性 + 公共 getter 方法
- 只读属性
只读属性的局限性在于只能在构造函数中赋值,无法在对象生命周期内通过类内部方法修改。PHP8.4 的不对称可见性 (Asymmetric Visibility),这一特性彻底改变了 PHP 中类属性的访问控制机制,允许开发者为属性的读取 (get) 和写入 (set) 操作设置不同的可见性级别,解决了长期以来属性封装的痛点问题。
语法格式:{读取可见性} {写入可见性}(set) {类型声明} $属性名;
示例:
php
<?php
declare(strict_types=1);
class Example
{
// $orderId 读取:public,写入:private(仅类内部可写)
// $amount 读取:protected(类和子类可见),写入:private(仅类内部可写)
// $status 读取:public,写入:protected(类和子类可写)
public function __construct(
public private(set) int $orderId,
protected private(set) float $amount,
public protected(set) string $status = 'pending'
)
{}
// 类内部方法可修改所有属性
public function updateStatus(string $newStatus) :void
{
$this->status = $newStatus; // 允许,写入可见性为protected
}
}
$order = new Example(1001, 99.99);
echo '<pre>';
// 读取操作(符合读取可见性)
var_dump($order->orderId); // 1001 (允许:public读取)
var_dump($order->status); // pending (允许:public读取)
// 写入操作(需符合写入可见性)
//$order->orderId = 1002; // 错误:private(set),外部不可写
//$order->status = 'shipped'; // 错误:protected(set),外部不可写
// 类内部方法修改
$order->updateStatus('shipped'); // 允许:类内部调用
var_dump($order->status); // shipped (成功修改)
不对称可见性并非无限制,PHP 引擎实施了以下重要约束以保证类型安全和代码一致性:
- 写入可见性不能宽于读取可见性:写入操作的可见性必须等于或更严格于读取可见性 。例:public 读取不能搭配 protected 写入以外的更宽松设置
private public(set) int $id(错误) - 必须有类型声明:不对称可见性仅适用于有显式类型的属性。例:
public private(set) $name(错误,无类型) - 静态属性支持:初始版本不支持静态属性,后续通过单独 RFC 补充支持。例:
public private(set) static int $counter(PHP8.4.1 + 有效) - 与 readonly 不兼容:不能与
readonly关键字同时使用 (语义冲突) 例:public private(set) readonly int $id(错误)
与属性钩子 (Property Hooks) 的结合。PHP8.4 同时引入了属性钩子特性,允许为属性添加自定义的 get/set 逻辑,与不对称可见性完美兼容。
示例:
php
<?php
declare(strict_types=1);
class Example {
// 不对称可见性:读公开,写私有
// 同时为 get 和 set 添加钩子
public private(set) string $name {
get {
return 'User: ' . $this->name; // 自动添加前缀
}
set {
if ($value === '') {
throw new InvalidArgumentException('Name cannot be empty');
}
$this->name = $value; // 直接写入后备字段,不会递归
}
}
public function __construct(string $name) {
$this->name = $name; // 触发 set 钩子(私有写)
}
}
$user = new Example('Alice');
echo $user->name; // 输出:User: Alice
// $user->name = 'Bob'; // 错误:Cannot modify private(set) property
子类重写父类的不对称可见性属性时,必须遵循以下规则,确保不破坏封装契约:
- 类型兼容性:子类属性必须与父类属性完全相同的类型(协变 / 逆变不允许)
- 读取可见性:子类读取可见性不能比父类更严格(可保持相同或更宽松)
- 子类写入可见性不能比父类更严格(可保持相同或更宽松)
- private (set) 限制:父类中声明为
private(set)的属性不能被子类重写(等效于 final 属性)
最佳实践:
- 实体类 / 值对象(最常用):User、Product、Order 等业务实体,ID、创建时间、唯一编码等属性,外部只能读取,仅类内部可初始化 / 修改。
- 服务类的内部状态属性:缓存服务、日志服务等,外部只读服务状态,内部根据操作动态更新
- 配置类的只读配置:应用配置、数据库配置等,加载后外部只读,仅初始化时内部设置。
- 与属性钩子结合(验证 / 格式化):需要对写入值做验证 / 格式化,但仍保持 "对外只读、对内可写"。
- 父子类共享的可写属性:基类定义属性,外部只读,子类可内部修改(用
public protected(set)) - 合理选择可见性组合:对外只读:
public private(set)、父子类共享写入:public protected(set)、内部组件可见:protected private(set)
三、#[Deprecated]
#[Deprecated]是 PHP 8.4 新增的内置属性,用于标记代码元素为已弃用,替代传统的trigger_error(E_USER_DEPRECATED)手动实现。适用目标:函数、方法、类常量 (8.4)、全局常量 (8.5)、特质 (8.5),错误级别为:E_USER_DEPRECATED (区别于内核的E_DEPRECATED),不会中断程序执行。
语法示例:
php
<?php
declare(strict_types=1);
// 简易示例
#[\Deprecated]
function simple_function(): void {}
// 完整示例
#[\Deprecated(message:"使用new_function()替代", since: "1.5.0")]
/**
* message:弃用说明,建议包含替代方案,会显示在警告信息中
* since:标记弃用起始版本,便于版本管理
*/
function old_function(): void {}
class FileUploader {
#[\Deprecated(message: "使用API_VERSION_3替代")]
public const API_VERSION_2 = "2.0";
public const API_VERSION_3 = "3.0";
#[\Deprecated(
message: "请使用uploadFile()方法替代,该方法支持文件验证",
since: "3.0.0"
)]
public function upload(string $path): void {
// 旧实现
}
public function uploadFile(string $path): void {
// 带验证的新实现
}
}
simple_function();// 错误:Deprecated: Function simple_function() is deprecated in
old_function();// 错误:Deprecated: Function old_function() is deprecated since 1.5.0, 使用new_function()替代 in
var_dump(FileUploader::API_VERSION_2); // 触发弃用警告 输出 "2.0"
$uploader = new FileUploader();
$uploader->upload("/tmp/file.txt"); // 触发弃用警告
四、ext-dom 功能和 HTML5 支持
历史痛点与解决方案:
传统 DOMDocument 依赖 libxml2 解析 HTML,而 libxml2 的 HTML 解析停留在 HTML 4 时代,存在三大问题:无法正确识别 <article>、<section> 等 HTML5 语义标签、解析规则与 HTML5 规范不一致,导致文档结构异常、处理嵌入式 JavaScript 等现代内容时容易出错。
PHP 8.4 引入 Lexbor 解析库(由 Alexander Borisov 开发),专为 HTML5 设计,具备以下优势:完全符合 WHATWG HTML 规范,支持所有 HTML5 元素与解析规则、高性能(C 语言实现,内存占用低,解析速度快)、原生支持 CSS 选择器查询,无需额外依赖、良好的错误恢复机制,处理 "标签汤(Tag Soup)" 更健壮。
关键设计:
Dom\HTMLDocument与Dom\XMLDocument均继承自抽象类Dom\Document- 旧
DOMDocument类现在也继承自Dom\Document,实现部分新特性兼容 - 新 API 使用静态构造函数而非
new关键字创建实例,更符合现代 PHP 实践
示例:
php
<?php
declare(strict_types=1);
// 1. 解析 HTML5 文档
$html = <<<'HTML'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>测试</title>
</head>
<body>
<div id="main"><p class="intro">PHP 8.4 新特性</p></div>
</body>
</html>
HTML;
$dom = Dom\HTMLDocument::createFromString($html);
echo '<pre>';
// 2. CSS 选择器查询
$intro = $dom->querySelector('#main .intro');
var_dump($intro->textContent); // 输出: PHP 8.4 新特性
// 3. 修改内容
$intro->innerHTML = 'PHP 8.4 <strong>DOM 新特性</strong>';
// 4. 操作类名
$intro->classList->add('highlight');
$intro->classList->remove('intro');
// 5. 序列化输出
echo $dom->saveHTML();
五、BCMath 对象 API
在 PHP 8.4 之前,处理高精度数学运算的唯一选择是调用 bcadd、bcsub 这类略显晦涩的过程式函数。PHP 8.4 引入了一个全新的、面向对象的 API------BcMath\Number 类。它不是一个简单的语法糖,而是为高精度计算带来了可读性、安全性和强大功能的全面提升。
核心特性:
- 运算符重载:支持
+、-、*、/、%、**等算术运算符及所有比较运算符,代码更直观易读 - 不可变性:所有计算操作返回新对象,原对象保持不变,避免副作用
- Stringable 接口:可直接在字符串上下文中使用(如
echo $num),自动转换为字符串表示 - 独立精度控制:不受
bcmath.scaleINI 指令影响,每个对象和操作可单独设置精度 - 全面方法支持:提供与传统 BCMath 函数对应的面向对象方法,覆盖所有高精度计算需求
基础示例:
php
<?php
declare(strict_types=1);
use BcMath\Number;
echo '<pre>';
// 整数初始化,scale 自动设为 0
$num1 = new Number(100);
var_dump($num1->value, $num1->scale); // string(3) "100", int(0)
// 字符串初始化,自动解析 scale
$num2 = new Number('123.456');
var_dump($num2->value, $num2->scale); // string(3) "123.456", int(3)
// 负数支持
$num3 = new Number('-789.00');
var_dump($num3->value, $num3->scale); // string(3) "-789.00", int(2)
// 仅支持 int/string 类型参数,浮点数以字符串形式传入,传入无效数字字符串会抛出 ValueError
//$num4 = new Number(0.1); // 错误:Argument #1 ($num) must be of type string|int, float given
//$num5 = new Number('abc123'); // 错误:Argument #1 ($num) is not well-formed
核心算数运算:
- 加法:
add($num, ?int $scale = null) - 减法:
sub($num, ?int $scale = null) - 乘法:
mul($num, ?int $scale = null) - 除法:
div($num, ?int $scale = null) - 取模:
mod($num, ?int $scale = null) - 幂运算:
pow($exponent, ?int $scale = null) - 幂模运算:
powmod($exponent, $modulus, ?int $scale = null) - 平方根:
sqrt(?int $scale = null) - 商余运算:
divmod($num, ?int $scale = null) - 向上取整:
ceil() - 向下取整:
floor() - 四舍五入:
round(int $precision = 0, RoundingMode $mode = RoundingMode::HalfAwayFromZero) - 比较两个数值:
compare($num, ?int $scale = null)
示例:
php
<?php
declare(strict_types=1);
use BcMath\Number;
echo '<pre>';
$num1 = new Number('5.5');
$num2 = new Number(5);
$num3 = new Number('5.0');
// 求和
var_dump($num1->add($num2, 2)); // BcMath\Number对象 value = 10.50 (scale=2)
var_dump($num1 + $num2); // BcMath\Number对象 value = 10.5
// 减法
var_dump($num1->sub($num2)); // BcMath\Number对象 value = 0.5
var_dump($num1 - $num2); // BcMath\Number对象 value = 0.5
// 乘法
var_dump($num1->mul($num2)); // BcMath\Number对象 value = 27.5
var_dump($num1 * $num2); // BcMath\Number对象 value = 27.5
// 除法
var_dump($num1->div($num2)); // BcMath\Number对象 value = 1.1
var_dump($num1 / $num2); // BcMath\Number对象 value = 1.1
// 取模
var_dump($num1->mod($num2)); // BcMath\Number对象 value = 0.5
var_dump($num1 % $num2); // BcMath\Number对象 value = 0.5
// 幂运算
var_dump($num2->pow(2));// BcMath\Number对象 value = 25
var_dump($num2 ** 2);// BcMath\Number对象 value = 25
// 平方根
var_dump(new Number(4)->sqrt()); // BcMath\Number对象 value = 2
// 商余运算
[$quotient, $remainder] = $num1->divmod($num2);
var_dump($quotient, $remainder); // BcMath\Number对象 value : quotient = 1 remainder = 0.5
// 幂模运算
var_dump(new Number(4)->powmod($num2, 5)); // BcMath\Number对象 value = 4
// 相等比较
var_dump($num2 == $num3); // true
// 严格比较
var_dump($num2 === $num3); // false
// 不等比较 != 或 <>
var_dump($num2 <> $num3); // false
var_dump($num2 != $num3); // false
var_dump($num2 !== $num3); // true
// 小于比较、大于比较、小于等于比较、大于等于比较
var_dump($num1 > $num2); // true
var_dump($num1 >= $num2); // true
var_dump($num2 < $num3); // false
var_dump($num2 <= $num3); // true
/** 复杂计算 链式调用 */
// 计算订单总价(含折扣和税费)
$price = new Number('99.99');
$quantity = new Number('3');
$discountRate = new Number('0.1'); // 10% 折扣
$taxRate = new Number('0.08'); // 8% 税费
$subtotal = $price->mul($quantity, 2); // 299.97
$discount = $subtotal->mul($discountRate, 2); // 29.997 → 30.00
$tax = ($subtotal->sub($discount))->mul($taxRate, 2); // 21.5976 → 21.60
$total = ($subtotal->sub($discount))->add($tax, 2); // 291.57
var_dump($total);
最佳实践:
- 确保 PHP 版本 ≥ 8.4,启用 BCMath 扩展(
extension=bcmath) - 始终使用字符串初始化小数
- 显式控制关键计算的精度,而非自动计算的 scale
- 链式调用简化复杂计算
- 利用不可变性进行安全计算
六、新的 array_*() 函数
PHP 8.4 正式引入了四个全新的数组操作函数:array_find()、array_find_key()、array_all() 和 array_any()。这些函数填补了 PHP 数组操作能力的空白,提供了更简洁、直观的方式来处理常见的数组查找和条件检查任务,避免了编写冗长的循环或使用不那么直观的现有函数组合。
1、array_find()
功能:按条件搜索第一个值。
语法:array_find(array $array, callable $callback): mixed
其中$array:待搜索的数组;$callback:回调函数,接收两个参数($value, $key),返回布尔值表示是否匹配
返回值:第一个使回调函数返回 true 的元素值 ,无匹配元素时返回 null
示例:
php
<?php
declare(strict_types=1);
$users = [
['id' => 1, 'name' => 'Alice', 'active' => false],
['id' => 2, 'name' => 'Bob', 'active' => true],
['id' => 3, 'name' => 'Charlie', 'active' => true]
];
// 查找第一个活跃用户 active = true
$firstActiveUser = array_find($users, fn($user) => $user['active']);
// 查找名字以 'C' 开头的用户
$userWithCName = array_find($users, fn($user) => str_starts_with($user['name'], 'C'));
// 查找不存在的条件
$inactiveUserOver30 = array_find($users, fn($user) => !$user['active'] && $user['id'] > 30);
echo '<pre>';
//输出:array(3) { ["id"]=> int(2) ["name"]=> string(3) "Bob" ["active"]=> bool(true) }
var_dump($firstActiveUser);
//输出:array(3) { ["id"]=> int(3) ["name"]=> string(7) "Charlie" ["active"]=> bool(true) }
var_dump($userWithCName);
var_dump($inactiveUserOver30); // null
2、array_find_key()
功能:按条件搜索第一个键。
语法:array_find_key(array $array, callable $callback): mixed
其中$array:待搜索的数组;$callback:回调函数,接收两个参数($value, $key),返回布尔值表示是否匹配
返回值:第一个使回调函数返回 true 的元素键名(保留原始键类型) ,无匹配元素时返回 null
示例:
php
<?php
declare(strict_types=1);
$products = [
'laptop' => ['price' => 999, 'in_stock' => true],
'phone' => ['price' => 699, 'in_stock' => false],
'tablet' => ['price' => 299, 'in_stock' => true]
];
// 查找第一个有库存的产品键名
$firstInStockKey = array_find_key($products, fn($products) => $products['in_stock']);
// 查找价格低于 500 的产品键名
$affordableProductKey = array_find_key($products, fn($product) => $product['price'] < 500);
echo '<pre>';
var_dump($firstInStockKey); // 输出:string(6) "laptop"
var_dump($affordableProductKey); // 输出:string(6) "tablet"
3、array_all()
功能:检查所有元素是否满足条件。
语法:array_all(array $array, callable $callback): bool
其中$array:待搜索的数组;$callback:回调函数,接收两个参数($value, $key),返回布尔值表示是否匹配
返回值:所有元素都使回调函数返回 true 时返回 true;只要有一个元素使回调函数返回 false,立即返回 false(短路求值);空数组时返回 true
示例:
php
<?php
declare(strict_types=1);
$numbers = [2, 4, 6, 8, 10];
// 检查所有数字是否为偶数
$allEven = array_all($numbers, fn($n) => $n % 2 === 0);
// 检查所有数字是否小于 5
$allSmall = array_all($numbers, fn($n) => $n < 5);
// 空数组测试
$emptyCheck = array_all([], fn($n) => $n > 0);
echo '<pre>';
var_dump($allEven); // 输出:bool(true)
var_dump($allSmall); // 输出:bool(false)
var_dump($emptyCheck); // 输出:bool(true)
4、array_any()
功能:检查是否存在满足条件的元素。
语法:array_any(array $array, callable $callback): bool
其中$array:待搜索的数组;$callback:回调函数,接收两个参数($value, $key),返回布尔值表示是否匹配
返回值:只要有一个元素使回调函数返回 true,立即返回 true(短路求值);所有元素都使回调函数返回 false 时返回 false;空数组时返回 false
示例:
php
<?php
declare(strict_types=1);
$emails = [
'user1@example.com',
'user2@test.org',
'user3@domain.net'
];
// 检查是否存在以 .org 结尾的邮箱
$hasOrgEmail = array_any($emails, fn($email) => str_ends_with($email, '.org'));
// 检查是否存在以 .gov 结尾的邮箱
$hasGovEmail = array_any($emails, fn($email) => str_ends_with($email, '.gov'));
// 空数组测试
$emptyCheck = array_any([], fn($email) => str_ends_with($email, '.com'));
echo '<pre>';
var_dump($hasOrgEmail); // 输出:bool(true)
var_dump($hasGovEmail); // 输出:bool(false)
var_dump($emptyCheck); // 输出:bool(false)