PHP 现代特性速查 写出更简洁安全的代码(第一篇)

基础你肯定掌握了。这个三部曲写给每天写 PHP 的人,帮你把代码写得更清楚、bug 更少、跑得更快。上篇讲那些能改变 API、DTO 和调用方式的现代特性。

默认你在用 PHP 8.x+。例子都很短,直接扔进 Laravel service 或普通 PHP 文件就能跑。

Attributes — 声明式、可发现的元数据(PHP 8.0)

替代了什么:脆弱的 docblock 和注解解析。

示例

php
#[Route(path: '/users', methods: ['GET'])]
class UserController { /* ... */ }

效果:反射读 attribute,启动时自动注册路由。IDE 能看到这些元数据,静态分析也能识别。

建议:attributes 适合框架接线(路由、验证、序列化),保持简单——复杂配置还是老实用 DTO。

Named Arguments — 自解释的函数调用(PHP 8.0)

替代了什么:容易搞错顺序的长参数列表。

示例

php
function connect(string $host, int $port, bool $tls = false) { /* ... */ }

// 更清晰,顺序无关
connect(port: 5432, host: 'db.internal', tls: true);

效果:调用像配置文件一样好读;加可选参数也不会破坏兼容性。

建议:工厂方法和 HTTP 客户端配置最适合——配合 readonly DTO 做不可变配置。

Constructor Property Promotion — 减少 service 和 DTO 的样板代码(PHP 8.0)

替代了什么:重复的属性声明和赋值。

示例

php
class Mailer {
    public function __construct(
        private LoggerInterface $log,
        private CacheInterface $cache
    ) {}
}

效果:构造函数简洁,打字少了拼写错误也少,属性自动带类型和注入。

建议:小 service 和 DTO 用这个——构造函数参数多了还是老实写工厂或 builder。

类型化属性(Typed Properties)— 尽早强制契约(PHP 7.4)

替代了什么:松散的 @var docblock 和混乱的 mixed 类型。

示例

php
class Order {
    public int $id;
    public ?DateTimeImmutable $shippedAt = null;
}

效果:赋错类型立刻抛 TypeError——领域 bug 早发现。

建议:配合静态分析(PHPStan/Psalm)在 CI 阶段就把问题拦住。

联合类型(Union Types)— 明确、灵活的 API(PHP 8.0)

替代了什么:模糊的 mixed 和不明确的类型提示。

示例

php
function find(string|int $id): ?User { /* ... */ }

效果:函数签名明确写出接受什么类型;静态工具能验证调用。

建议:union 要有意义——别动不动就 string|int|float|bool,除非真需要。

交叉类型(Intersection Types)— 更严格的多能力契约(PHP 8.1)

替代了什么:运行时检查对象是否实现多个接口。

示例

php
function process(Reader&Logger $obj) {
    // $obj 同时是 Reader 和 Logger
}

效果:编译时就能保证对象有你要的能力。

建议:装饰器和适配器最适合,确保它们同时满足多个接口。

Enums — 用领域安全的值替代魔法字符串(PHP 8.1)

替代了什么:容易出错的常量和字符串。

示例

php
enum PaymentStatus: string {
    case PENDING = 'pending';
    case PAID    = 'paid';
    case FAILED  = 'failed';
}

$status = PaymentStatus::PAID;

效果:match 能穷举所有情况、重构更安全、日志也更清楚。

建议:持久化用 backed enums(string/int);常见的领域逻辑直接写在 enum 方法里。

只读属性和只读类(Readonly)— 不可变 DTO(属性:PHP 8.1;类:PHP 8.2)

替代了什么:手写的不可变对象和意外的变更 bug。

示例

php
readonly class UserDTO {
    public function __construct(
        public int $id,
        public string $email
    ) {}
}

效果:构造完就不能改了——事件、配置、API 响应都适合。

建议:跨进程传的数据(队列、事件)优先用 readonly。

一等公民可调用对象(First-class Callables)— 简洁、零样板的回调(PHP 8.1)

替代了什么:冗长的匿名函数或基于字符串的 callable。

示例

php
$upper = strtoupper(...);
$names = array_map($upper, ['alice','bob']);

效果:管道好读,开销小。

建议:配合 array_map/array_filter 用,意图清楚。不需要捕获变量时别用闭包。

组合使用:真实例子

假设写个控制器动作,把这些特性组合起来:

php
#[Route(path: '/orders', methods: ['POST'])]
final class CreateOrderAction {
    public function __construct(
        private OrderService $service,
        private LoggerInterface $logger
    ) {}

    public function __invoke(CreateOrderDTO $dto): JsonResponse {
        $order = $this->service->create($dto);
        $this->logger->info('order.created', ['id' => $order->id]);
        return new JsonResponse(['id' => $order->id], 201);
    }
}

这段代码把 attributes(路由)、constructor promotion(注入)、readonly DTO(不可变)、typed properties 和 enums(领域安全)组合在一起,代码简洁、一看就懂、还更安全。

高级技巧 — 老手怎么组合这些特性

验证管道:DTO + attributes + 静态分析,能在编译期验证的就别等运行时。

领域值对象:少用松散数组,多用小 readonly 值对象——类型化属性能帮你挡住下游的 bug。

Match + Enums:别用布尔标志和字符串 switch 了,enum 配 match 穷举,编译器帮你找漏掉的分支。

交叉类型给适配器:适配器要同时实现 Cacheable&Loggable 才能传给基础设施。

一等公民可调用对象优化 map:没有闭包分配开销,快一点也清楚一点。

什么时候别用这些特性(注意事项)

attributes 太多会拖慢反射 — 做接线可以,别在字段上堆一堆元数据。

Named arguments 在 DI 容器里可能出问题 — 调用时用它提升可读性没问题,写库给别人用就别这么干。

到处 readonly 开发时很烦 — DTO 和事件用 readonly 有意义,service 对象就别折腾了。

升级路线

还在 PHP < 7.4:先升到 7.4,类型化属性值得。

想提升生产力:直接上 8.1(enums、只读属性、一等公民可调用对象、交叉类型)。

8.2 加了只读类和其他改进——有用但不急。

最后

这些特性不是玩具,它们改变你建模数据、强制约束、理解系统的方式。用好了,attributes、enums、类型化属性能让代码更稳、团队更快。

代码库还在用魔法字符串、靠 docblock 或到处传可变数组?选一个特性(enums 或 readonly DTO)先迁移一个模块试试。效果好了,后面就好办了。

准备用哪个特性?说说你的痛点,我帮你找投入小、收益大的改法。

CatchAdmin
后端开发工程师,前端入门选手,略知相关服务器知识,偏爱❤️ Laravel & Vue
本作品采用《CC 协议》,转载必须注明作者和本文链接