别再写 $value ?? new Object() 使用PHP 8.1 的New in Initializers 特性替代
日常写 PHP 代码时,我们经常遇到一个很繁琐的问题:类的依赖需要设置默认对象,只能先把参数设为可空类型,再在构造函数内部判空、手动实例化默认类。不仅模板代码一大堆,类型声明也不严谨,明明业务不允许为空,代码却要写可空兼容,可读性和规范性都很差。
PHP 8.1 推出的 New in Initializers 特性,刚好解决了这个痛点。简单说就是支持在初始化场景直接写 new 实例化对象,搭配 PHP8 属性提升,能大幅精简代码、强化类型约束。很多人只知道能用,却不清楚适用场景、限制细节和新旧写法的隐性差异。这篇文章我结合大量实战案例,完整讲透这个特性的用法、优势、坑点和最佳实践。
一、在参数默认值中使用 new
这是 PHP 8.1 初始化器最常见的使用场景。
假设我们正在实现一个状态机。创建状态机时,如果调用方没有指定初始状态,系统就自动使用 InitialState。
PHP 8.1 之前的写法
在 PHP 8.1 之前,不能直接使用对象作为参数默认值,因此一般需要将参数声明为可空类型,再在构造函数中完成判空和初始化。
<?php
declare(strict_types=1);
interface State
{
public function name(): string;
}
final class InitialState implements State
{
public function name(): string
{
return 'initial';
}
}
final class DraftState implements State
{
public function name(): string
{
return 'draft';
}
}
final class MyStateMachine
{
private State $state;
public function __construct(?State $state = null)
{
$this->state = $state ?? new InitialState();
}
public function currentState(): State
{
return $this->state;
}
}
$defaultMachine = new MyStateMachine();
echo $defaultMachine->currentState()->name();
// initial
$draftMachine = new MyStateMachine(new DraftState());
echo $draftMachine->currentState()->name();
// draft
这种写法没有问题,但存在一些模板化代码:
- 构造函数参数必须声明为
?State。 - 属性和构造函数参数需要分别声明。
- 构造函数内部还要执行一次判空和赋值。
- 从类型声明上看,参数允许传入 null,但业务上最终并不希望属性是 null。
二、PHP 8.1 的简化写法
从 PHP 8.1 开始,可以直接将new InitialState()设置为参数的默认值。
结合 PHP 8.0 引入的构造函数属性提升,整个类可以明显简化:
<?php
declare(strict_types=1);
interface State
{
public function name(): string;
}
final class InitialState implements State
{
public function name(): string
{
return 'initial';
}
}
final class DraftState implements State
{
public function name(): string
{
return 'draft';
}
}
final class MyStateMachine
{
public function __construct(
private State $state = new InitialState(),
) {
}
public function currentState(): State
{
return $this->state;
}
}
$defaultMachine = new MyStateMachine();
echo $defaultMachine->currentState()->name();
// initial
$draftMachine = new MyStateMachine(new DraftState());
echo $draftMachine->currentState()->name();
// draft
现在,参数的类型、属性声明和默认对象都集中在构造函数签名中:
private State $state = new InitialState()
开发者不需要进入构造函数内部,就能立即知道:
- 这个类依赖一个 State 对象;
- 调用方可以传入自定义状态;
- 如果没有传入,则默认使用 InitialState;
- $state 在类的内部始终是一个 State 对象,不会是 null。
三、默认对象什么时候创建?
参数默认值中的 new 表达式只会在调用方没有传入对应参数时执行。
$machine = new MyStateMachine();
上面的代码没有传入 $state,因此 PHP 会执行:
new InitialState()
如果调用方已经传入对象:
$machine = new MyStateMachine(new DraftState());
默认的 new InitialState() 就不会执行。
可以通过下面的示例观察这一行为:
<?php
declare(strict_types=1);
final class ConsoleLogger
{
public function __construct()
{
echo "ConsoleLogger created\n";
}
}
final class Application
{
public function __construct(
private ConsoleLogger $logger = new ConsoleLogger(),
) {
}
}
$logger = new ConsoleLogger();
$app1 = new Application($logger);
// 只会创建上面手动传入的 ConsoleLogger
$app2 = new Application();
// 没有传入参数,此时才会创建默认的 ConsoleLogger
这意味着参数默认值不是在类定义加载时提前创建的,而是在实际需要默认值时求值。
另外,每次省略参数都会创建一个新的对象:
$app1 = new Application(); $app2 = new Application();
这里的 $app1 和 $app2 会分别获得不同的 ConsoleLogger 实例。它并不是一个在多个对象之间共享的单例。
四、新旧写法并不完全等价
这一点非常重要。
旧写法通常将参数声明为可空类型:
public function __construct(?State $state = null)
{
$this->state = $state ?? new InitialState();
}
因此,下面两种调用都会使用默认状态:
new MyStateMachine(); new MyStateMachine(null);
但是 PHP 8.1 的简化写法使用的是非空类型:
public function __construct(
private State $state = new InitialState(),
) {
}
此时可以省略参数:
new MyStateMachine();
但不能显式传入 null:
new MyStateMachine(null);
以上代码会抛出 TypeError。
两种写法的语义区别是:
- 省略参数:使用默认对象;
- 显式传入 `null`:不再被视为省略参数,而是类型错误。
在大多数业务场景中,这种更严格的类型约束反而是优势,因为它准确表达了“必须是一个 State,只是允许调用方不传”的设计意图。
如果业务确实需要区分“未传参数”和“显式传入 null”,则需要保留可空类型,并根据业务规则单独处理。
五、在 Attribute 中使用 new
PHP 8.1 还允许在 Attribute 参数中使用 new 表达式。
这对验证器、ORM、序列化器、路由和依赖注入等基于元数据的框架非常有价值,因为我们可以直接在 Attribute 中声明对象化的配置。
下面实现一个简单的数组字段验证器。
定义验证规则接口
<?php
declare(strict_types=1);
interface ValidationRule
{
public function validate(mixed $value): bool;
public function message(): string;
}
实现具体规则
final class Required implements ValidationRule
{
public function validate(mixed $value): bool
{
return $value !== null && $value !== '';
}
public function message(): string
{
return '该字段不能为空';
}
}
final class ValidEmail implements ValidationRule
{
public function validate(mixed $value): bool
{
return is_string($value)
&& filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
}
public function message(): string
{
return '该字段必须是有效的邮箱地址';
}
}
final class ValidString implements ValidationRule
{
public function validate(mixed $value): bool
{
return is_string($value);
}
public function message(): string
{
return '该字段必须是字符串';
}
}
定义 Attribute
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class ValidArray
{
/**
* @param array<string, list<ValidationRule>> $rules
*/
public function __construct(
public readonly array $rules,
) {
}
}
在 Attribute 中创建规则对象
final class CreateEmailsRequest
{
/**
* @param list<array{name?: mixed, email?: mixed}> $people
*/
public function __construct(
#[ValidArray(
rules: [
'email' => [
new Required(),
new ValidEmail(),
],
'name' => [
new Required(),
new ValidString(),
],
],
)]
public readonly array $people,
) {
}
}
在 PHP 8.1 中,Attribute 参数不再只能是简单的标量或数组,还可以包含由 new 创建的对象。
这样一来,验证规则可以直接通过对象表达:
new Required() new ValidEmail() new ValidString()
相比字符串形式的规则:
'required|email'
对象化规则通常具有以下优势:
- 可以获得更明确的类型检查;
- IDE 更容易提供自动补全和重构支持;
- 规则可以通过构造函数接收配置;
- 不需要额外解析字符串;
- 复杂规则更容易组合和复用。
六、通过反射读取并执行 Attribute
为了让示例更加完整,下面使用反射读取 ValidArray,并执行其中的验证规则。
<?php
declare(strict_types=1);
final class RequestValidator
{
/**
* @return array<string, list<string>>
*/
public function validate(object $request): array
{
$errors = [];
$reflection = new ReflectionObject($request);
foreach ($reflection->getProperties() as $property) {
$attributes = $property->getAttributes(ValidArray::class);
foreach ($attributes as $attribute) {
/** @var ValidArray $configuration */
$configuration = $attribute->newInstance();
$value = $property->getValue($request);
if (!is_array($value)) {
$errors[$property->getName()][] = '该字段必须是数组';
continue;
}
foreach ($value as $index => $item) {
if (!is_array($item)) {
$errors["{$property->getName()}.{$index}"][] =
'数组元素格式错误';
continue;
}
foreach ($configuration->rules as $field => $rules) {
$fieldValue = $item[$field] ?? null;
foreach ($rules as $rule) {
if (!$rule->validate($fieldValue)) {
$errorKey = sprintf(
'%s.%d.%s',
$property->getName(),
$index,
$field,
);
$errors[$errorKey][] = $rule->message();
}
}
}
}
}
}
return $errors;
}
}
使用验证器:
$request = new CreateEmailsRequest([ [ 'name' => 'Tom', 'email' => 'tom@example.com', ], [ 'name' => '', 'email' => 'invalid-email', ], ]); $validator = new RequestValidator(); $errors = $validator->validate($request); print_r($errors);
输出结果类似:
Array ( [people.1.name] => Array ( [0] => 该字段不能为空 ) [people.1.email] => Array ( [0] => 该字段必须是有效的邮箱地址 ) )
需要注意,Attribute 中声明的对象通常会在反射调用ReflectionAttribute::newInstance()、实例化对应 Attribute 时创建,而不是在 PHP 解析类文件时立即创建。
七、带参数的初始化对象
new 初始化器并不局限于无参构造函数,也可以传入常量表达式。
例如,定义一个长度验证规则:
final class StringLength implements ValidationRule
{
public function __construct(
private readonly int $min,
private readonly int $max,
) {
}
public function validate(mixed $value): bool
{
if (!is_string($value)) {
return false;
}
$length = mb_strlen($value);
return $length >= $this->min && $length <= $this->max;
}
public function message(): string
{
return sprintf(
'字符串长度必须在 %d 到 %d 个字符之间',
$this->min,
$this->max,
);
}
}
然后直接在 Attribute 中配置:
#[ValidArray( rules: [ 'name' => [ new Required(), new ValidString(), new StringLength(min: 2, max: 50), ], 'email' => [ new Required(), new ValidEmail(), ], ], )] public readonly array $people;
这种写法将规则和配置放在一起,阅读代码时可以直接理解字段的验证要求。
八、在静态变量中使用 new
PHP 8.1 还允许使用 new 初始化函数内部的静态变量。
<?php
declare(strict_types=1);
final class Formatter
{
public function format(string $value): string
{
return trim(strtolower($value));
}
}
function normalize(string $value): string
{
static $formatter = new Formatter();
return $formatter->format($value);
}
echo normalize(' HELLO ');
// hello
静态变量只会在函数第一次执行到该声明时初始化,后续调用会继续使用同一个对象。
不过,这种方式会在函数内部形成隐藏的共享状态。它比较适合无状态工具对象,不建议用来存储与请求或用户相关的可变数据。
九、不能直接作为普通属性的默认值
虽然该特性叫“初始化器中的 new”,但它并没有让 new 可以出现在所有初始化位置。
下面的普通属性默认值仍然是错误的:
final class MyStateMachine
{
private State $state = new InitialState();
}
类常量中也不能这样使用:
final class StateRegistry
{
public const DEFAULT_STATE = new InitialState();
}
如果需要为对象属性提供默认实例,推荐使用构造函数参数默认值与属性提升:
final class MyStateMachine
{
public function __construct(
private State $state = new InitialState(),
) {
}
}
虽然$state最终仍然是一个属性,但new InitialState()实际上是构造函数参数的默认值,而不是普通属性的默认值。
十、new 表达式并非可以接收任意内容
初始化器需要能够在不依赖运行时局部状态的情况下完成求值,因此其中可以使用的表达式仍然受到限制。
不能传入普通变量
下面的代码不可用:
$defaultName = 'default';
function createService(
Service $service = new Service($defaultName),
): void {
}
参数默认值不能依赖函数外部的普通变量。
不能调用普通函数或方法
下面的写法同样不可用:
function createService(
Service $service = new Service(getDefaultName()),
): void {
}
也不能通过静态方法动态创建默认值:
function createService(
Service $service = ServiceFactory::create(),
): void {
}
不能使用参数展开
下面的代码不被支持:
function createService(
Service $service = new Service(...['default']),
): void {
}
不能实例化匿名类
下面的代码也不能作为初始化器:
function createService(
object $service = new class {
},
): void {
}
简单来说,初始化器适合使用明确、稳定、可以独立求值的对象创建表达式,不适合承载复杂的运行时逻辑。
十一、适合使用初始化器的场景
Null Object 模式
interface Logger
{
public function log(string $message): void;
}
final class NullLogger implements Logger
{
public function log(string $message): void
{
// 默认不记录日志
}
}
final class OrderService
{
public function __construct(
private Logger $logger = new NullLogger(),
) {
}
}
相比?Logger $logger = null,NullLogger 可以让业务代码始终调用一个真正的 Logger,避免在各处重复判空。
默认值对象
final class Pagination
{
public function __construct(
public readonly int $page = 1,
public readonly int $pageSize = 20,
) {
}
}
final class ProductQuery
{
public function __construct(
private Pagination $pagination = new Pagination(),
) {
}
}
默认策略对象
interface RetryStrategy
{
public function attempts(): int;
}
final class NoRetry implements RetryStrategy
{
public function attempts(): int
{
return 0;
}
}
final class ApiClient
{
public function __construct(
private RetryStrategy $retryStrategy = new NoRetry(),
) {
}
}
Attribute 配置
适用于以下基于元数据的功能:
- 参数验证
- ORM 字段映射
- 序列化规则
- 权限配置
- 路由配置
- 事件监听器配置
十二、什么时候不应该使用?
new 参数默认值虽然简洁,但并不适合所有依赖。
如果对象存在以下情况,应优先考虑依赖注入容器或工厂:
- 创建成本较高;
- 依赖数据库连接或网络连接;
- 需要读取环境变量;
- 依赖运行时配置;
- 需要复杂的对象装配;
- 需要统一管理对象生命周期;
- 需要根据业务条件选择不同实现。
例如,不建议这样创建数据库连接:
final class UserRepository
{
public function __construct(
private DatabaseConnection $connection =
new DatabaseConnection('localhost', 'root', 'password'),
) {
}
}
数据库连接属于基础设施依赖,其配置和生命周期通常应该交给依赖注入容器管理。
new 初始化器更适合轻量、无副作用、默认行为明确的值对象、策略对象和 Null Object。
总结
PHP 8.1 的New in Initializers特性让 new 表达式可以用于参数默认值、Attribute 参数和静态变量等初始化场景。
它的主要价值包括:
- 减少构造函数中的判空和赋值代码;
- 配合构造函数属性提升,让类定义更加紧凑;
- 使用非空类型准确表达依赖关系;
- 仅在省略参数时创建默认对象;
- 让 Attribute 支持更加复杂的对象化配置;
- 提升代码的声明性、可读性和类型安全性。
需要特别注意的是,下面两种构造函数并不完全等价:
public function __construct(?State $state = null)
public function __construct( State $state = new InitialState(), )
前者允许调用方显式传入 null,后者只允许调用方省略参数,不能传入 null。
如果项目已经升级到 PHP 8.1,可以优先检查构造函数中的这类代码:
$this->dependency = $dependency ?? new DefaultDependency();
对于轻量且没有外部副作用的默认依赖,可以考虑改写为:
public function __construct(
private Dependency $dependency = new DefaultDependency(),
) {
}
这样既减少了模板代码,也能让类的依赖和默认行为一目了然。
总结
总的来说,PHP8.1 的 New in Initializers 不是花里胡哨的语法糖,而是能实实在在优化代码结构、提升类型严谨度的实用特性。它彻底解决了旧版本默认对象初始化的尴尬问题,告别了构造函数大量判空、兼容可空类型的冗余写法,让类依赖、默认配置一目了然。
但我们也不能无脑滥用,它有明确的使用边界:不支持普通属性、常量初始化,不能写复杂动态逻辑,也不适合数据库连接这类重型、有副作用的依赖。只要遵守适用场景,多用于策略类、空对象模式、验证规则、轻量工具类默认初始化,搭配 Attribute 对象化配置,就能让代码更简洁、更规范、更易维护。
升级了 PHP8.1 及以上版本的项目,完全可以把项目里老旧的默认值初始化代码逐步重构,用更优雅、更严谨的新写法替代传统判空逻辑。
以上关于别再写 $value ?? new Object() 使用PHP 8.1 的New in Initializers 特性替代的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » 别再写 $value ?? new Object() 使用PHP 8.1 的New in Initializers 特性替代
微信
支付宝