最近把一个老项目从 PHP 7.4 迁到 PHP 8.3,发现很多以前要写好几行判断的地方,现在直接一个函数就够了。今天不聊那些概念性的东西,就说说让我印象最深的三个特性:json_validate()、类型化类常量,以及 #[Override] 属性。都是实战里能用上的,顺带省了时间。
一、json_validate:只验证不解析,省心多了
以前我要判断一个字符串是不是合法的 JSON,总是习惯像下面这样写:
function isJson($string) {
$data = json_decode($string, true);
return (json_last_error() === JSON_ERROR_NONE);
}
明明只是想判断“是不是JSON”,结果把整个数据都解析了一遍,浪费内存。而且如果字符串特别大,还会慢悠悠地构建数组,甚至可能因为深度太深报错。
PHP 8.3 新增了 json_validate(),专门干这个事:它只校验语法,不生成数组对象。用法很简单:
if (json_validate($jsonString)) {
// 是有效 JSON
} else {
// 不是 JSON,或格式错误
}
有人可能会问:那我要解析 JSON 怎么办?还是用 json_decode。只是在你只需要验证数据的场景下,json_validate 更适合。比如接收异步传送的数据时,我先用 json_validate 快速检查一下,发现不对就直接打回,根本不会执行后面的 json_decode,这样接口的安全性更高。
一个真实例子:读取上传的JSON配置
之前写了一个导入插件配置的功能,用户上传 .json 文件,我需要检查文件内容是否合法。老代码像这样:
$data = json_decode(file_get_contents($_FILES['file']['tmp_name']), true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo "配置文件格式不对!";
}
换成新方法之后,逻辑就清楚了:
$content = file_get_contents($_FILES['file']['tmp_name']);
if (!json_validate($content)) {
echo "配置文件格式不对!";
} else {
$data = json_decode($content, true);
// 正常处理
}
通过 json_validate 先拦一道,避免了无意义的解码。虽然性能差异在几百KB的文件上几乎感觉不出来,但至少代码表达的意思更准确了。
二、类型化类常量:常量也讲究数据类型
PHP 8.3 还支持给类常量添加类型声明。以前常量就只是一个“简单的值”,你没法规定它是 int 还是 string。现在可以了。比如我们写一个支付回调的状态类,以前是这么写的:
class PaymentStatus {
const SUCCESS = 0;
const FAILED = 1;
const PENDING = 2;
}
如果不小心写成 const SUCCESS = '0'; 也能过,但比较起来就容易踩坑。用类型化常量,就能锁定类型:
class PaymentStatus {
public const int SUCCESS = 0;
public const int FAILED = 1;
public const int PENDING = 2;
}
这样如果哪天代码里试图给 SUCCESS 赋一个字符串值,PHP 引擎直接报错,不需要等你运行到某个诡异的分支才发现问题。
搭配使用:枚举还是常量?
很多人说 PHP 8.1 有枚举,为什么还要用类常量?主要是枚举有时候太“重”,简单的状态标志用类型化常量更轻量。实际项目中,我经常用联合类型和常量结合,比如:
class UserRoles {
public const string ADMIN = 'admin';
public const string EDITOR = 'editor';
public const string VIEWER = 'viewer';
}
然后在方法参数里用:
function checkPermission(string $role): void {
if ($role === UserRoles::ADMIN) {
// ...
}
}
虽然不加类型好像也没啥,但当别人接手代码,一看到 public const string ADMIN 就知道这个常量的值肯定是字符串,不会乱传。
三、#[Override] 属性:重构时不再慌
PHP 8.3 引入了 #[Override] 属性,用来标记一个方法是不是重写了父类或接口的方法。以前我们重写方法全靠自觉,如果哪天父类把方法改名了,子类的“重写”方法就变成了一个普通方法,没人告诉你,直到运行时报错才一头雾水。
用了 #[Override] 后,PHP 会在编译阶段帮你检查:如果父类或接口没有这个名字的方法,就会抛致命错误。相当于给你上了道保险。
拿我最近重构的一段代码来说吧。我有个抽象类 BaseParser:
abstract class BaseParser {
public function parse(string $input) {
return $this->doParse($input);
}
abstract protected function doParse(string $input): array;
}
以前有子类这样写:
class JsonParser extends BaseParser {
protected function doParse(string $input): array {
return json_decode($input, true) ?? [];
}
}
看起来没问题。但如果某天我把抽象方法名改成了 parseData,而 JsonParser 里还是 doParse,那这个方法就不会被调用,而且因为子类里有 doParse,也不会报错。数据就这样悄悄没了。
加了 #[Override] 以后:
class JsonParser extends BaseParser {
#[Override]
protected function doParse(string $input): array {
return json_decode($input, true) ?? [];
}
}
如果你把父类的方法名改了,PHP 编译器会直接报错“方法 JsonParser::doParse() 不能覆盖任何方法”,你再也不会碰到那种找不到原因的bug了。我现在基本上在重写方法上都加了这一行,成本几乎为零,好处却是实打实的。
四、三个特性结合:一个更干净的API响应处理
光说单个特性没意思,我把它们结合起来,写了一个简单的HTTP响应处理类。这可能是这三样东西最典型的配合场景。
class ApiResponse {
public const int OK = 200;
public const int BAD_REQUEST = 400;
public function __construct(
private string $rawBody
) {}
#[Override]
public function __toString(): string {
return $this->rawBody;
}
public function isValidJson(): bool {
return json_validate($this->rawBody);
}
public function toArray(): array {
if (!$this->isValidJson()) {
throw new InvalidArgumentException('响应内容不是合法的 JSON');
}
return json_decode($this->rawBody, true) ?? [];
}
}
在控制器里,就这么写:
$response = new ApiResponse($httpResponseBody);
$code = $httpStatusCode;
if ($code === ApiResponse::OK && $response->isValidJson()) {
$result = $response->toArray();
} else {
// 记录错误日志
}
代码变得特别直白,而且不依赖额外的判断。类型化常量保证了 OK 一定是 int,不会出现 "200" 这种意外。抽象方法重写检查也让我在改动基类时放心不少。
五、升级要注意的小坑
虽然这些特性很香,但升级到 PHP 8.3 还是有几点要留意:
json_validate()对于 JSON 符号:它严格要求 UTF-8,如果字符串里有非法字符,会返回 false。这点跟json_decode的行为也一致,不措手不及。- 类型化类常量目前不支持
private?其实支持,但要注意接口常量默认是 public,且在接口中不能用private。用的时候别搞混。 #[Override]属性只对“重写父类或接口方法”生效。如果你只是在同一个类里定义了一个普通方法,别加这个属性,不然直接报错。
别问我怎么知道的,都是泪。
六、总结
PHP 8.3 没有搞什么惊天动地的大改版,但像 json_validate、类型化常量、#[Override] 这种细节,确实能提高代码质量。尤其是 #[Override],我觉得每个项目都值得加上,毕竟“继承”这种东西,最大的问题就是太隐晦了。
最后还是建议你,如果项目还在旧的 PHP 版本上,趁着不忙,把版本提一提。不光是性能提升,这些语法糖真的能让编码体验好很多。就算暂时不想升,也可以看看新特性的文档,以后写代码多一点点选择。

