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

三部曲完结篇,讲区分老手和新手的高级模式:长期进程的内存管理、现代并发原语、生产环境的运维改进、小而关键的 API 改进。

弱映射和弱引用(WeakMap & WeakReference)— 防止内存泄漏(PHP 8.0)

问题:普通数组给对象附加元数据会阻止垃圾回收,长期进程(worker、ReactPHP 服务器)慢慢泄漏内存。

什么时候用:缓存、每对象元数据、监听器注册表,元数据不该让对象一直活着。

php
$cache = new WeakMap();

$user = new User(id: 123);
$cache[$user] = expensiveOperation($user);

// 稍后...
unset($user);
// $cache 条目自动移除,GC 能释放内存

效果:临时元数据在主对象回收时消失——长期进程内存稳定。

建议:WeakMap 用对象身份(不是 ID)做键的临时缓存。

纤程(Fibers)— 异步 I/O 的绿色线程(PHP 8.1)

问题:轻量级并发时的回调地狱或复杂 promise 链。

什么时候用:自定义异步层、数千并发 I/O 的 CLI 工具、事件循环(Amp、ReactPHP)集成。

php
$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('paused');
    echo "Resumed with: $value\n";
});

$val = $fiber->start();       // 启动,返回 'paused'
$fiber->resume('hello');      // 恢复,打印 "Resumed with: hello"

效果:写非阻塞、线性的代码流,干净地 yield 和 resume。

建议:优先用成熟的异步库(Amp、ReactPHP),它们基于 Fibers——别自己重新实现多路复用器。

自定义会话处理器(Custom Session Handler)— Session 扩展存储

问题:文件 session 本地行,多节点和扩展时就崩了。

什么时候用:水平扩展应用,必须共享 session 状态。

php
class RedisSessionHandler implements SessionHandlerInterface {
    public function __construct(private Redis $redis) {}

    public function read(string $id): string {
        return (string) $this->redis->get("sessions:$id");
    }
    public function write(string $id, string $data): bool {
        return $this->redis->setex("sessions:$id", 3600, $data);
    }
    // 实现 open, close, destroy, gc...
}

session_set_save_handler(new RedisSessionHandler($redis));

效果:快速集中的 session(Redis、memcached、数据库),实例重启后还在,配合负载均衡器。

建议:用固定键前缀(sessions:)和专用 Redis DB 存 session。

预加载(Preloading)— OPcache 性能提升(PHP 7.4+)

问题:高吞吐应用重复编译 opcode 有开销。

什么时候用:稳定的生产代码,知道哪些类是热点。

php
// config/preload.php
opcache_compile_file('/app/vendor/autoload.php');
opcache_compile_file('/app/src/Service/Foo.php');
// php.ini: opcache.preload=/path/to/config/preload.php

效果:热类预编译到共享内存,请求延迟更低、启动成本更小。

建议:配合 composer install --classmap-authoritative 保持 preload 列表紧凑。

Override 特性标注 — 更安全重构(PHP 8.3)

问题:父类或接口方法名/签名改了,出现静默 bug。

什么时候用:子类或实现类覆盖父方法时。

php
interface LoggerInterface {
    public function log(string $message): void;
}

class FileLogger implements LoggerInterface {
    #[\Override]
    public function log(string $message): void {
        // ...
    }
}

效果:PHP 8.3 在 #[Override] 没实际覆盖父类/接口方法时报编译错误——重构的免费护栏。

建议:广泛用 #[Override];PHP ≥ 8.3 的静态分析和 CI 能早点抓回归。

(注意:#[Override] 从 PHP 8.3 开始引擎强制;旧版本上是空的。)

可字符串化接口(Stringable)— 字符串化对象类型提示(PHP 8.0)

问题:接受字符串或带 __toString() 对象的 API 要笨拙检查。

什么时候用:日志、模板、接受字符串或能转字符串的领域对象的 API。

php
function writeToLog(Stringable|string $message): void {
    file_put_contents('/tmp/log', (string) $message . PHP_EOL, FILE_APPEND);
}

writeToLog("plain text");
writeToLog(new class { public function __toString(){ return "object text"; }});

效果:函数签名更清楚,编译时就知道允许什么输入。

建议:Stringable 配 union types 让 API 灵活又有类型。

组合使用 — 生产模式

假设高吞吐订单处理器用了三篇的模式。readonly 命令处理器(第 1 篇)用 WeakMap 做每请求缓存,match 做状态映射(第 2 篇),Redis 做快速查找(第 3 篇):

php
#[AsMessageHandler]
readonly class ProcessOrderHandler {
    public function __construct(
        private OrderRepository $repo,
        private Redis $redis,
        private LoggerInterface $logger
    ) {}

    public function __invoke(ProcessOrder $cmd): void {
        static $ctx = new WeakMap();
        $priority = match ($cmd->type) {
            'express' => 'high',
            default => 'normal',
        };
        $this->logger->info('order.processing', [
            'id' => (string) $cmd->orderId,
            'priority' => $priority,
        ]);
        $this->redis->setex("order:summary:{$cmd->orderId}", 3600, serialize($this->repo->summary($cmd->orderId)));
    }
}

这就是语言特性配运维模式写出的紧凑、健壮、生产级代码。

速查表(最终版)

特性使用场景PHP 版本
弱映射避免泄漏8.0
纤程异步 IO8.1
会话处理器扩展 sessionlegacy
预加载加速 OPcache7.4
Override安全重构8.3
可字符串化接口字符串 API8.0

最后 — 架构工具,不是玩具

三篇的特性不是语法糖——是架构工具。

第一篇:声明意图(类型、attributes、enums)
第二篇:表达逻辑(match、生成器、null-safe)
第三篇:构建弹性系统(弱映射、纤程、预加载)

选一个能解决代码库痛点的模式,迁移一个模块试试。ROI 立竿见影——后面的路就清楚了。

三部曲完结。去做点厉害的东西吧。想好先用哪个特性了吗?告诉我你的痛点,我帮你找个小迁移方案。

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