PHP “快死了”这句话已经喊了很多年了,如果真有来世,它的简历应该已经相当可观了。
你大概见过这些论调:"PHP 过时了"、"现在没人用 PHP 做正经系统了"、"只有老项目还在用"。这些说法也不算全错——确实有大量遗留 PHP 代码还i在运行。但还有另一个现实很少被提及:PHP 仍然在驱动大量生产环境的后端系统,新的 PHP 项目也在不断出现,因为团队想要的东西与五年、十年前一样:
我喜欢这类问题,因为它逼你把话说清楚。不是"我喜欢用",不是"它很流行",而是你在生产代码中能实际指出的工程优势。
下面是我的回答,写给两类读者:
我会尽量用平实的语言,但不会回避技术细节。真实的系统本来就是技术性的。目标是让这些技术内容变得易懂且实用。
开发者比较编程语言时,讨论经常跑偏到性能基准、语法偏好或者互联网文化。但当你在构建 API 或 Web 后端时,"优势"通常意味着一些更无聊——也更重要的东西:
这就是 PHP 最擅长的领域。不是因为它最优雅,而是因为它的形状刚好契合大多数 Web 后端。
带着这个思路,来看 PHP 在 2026 年的三大优势。
大多数后端都是 HTTP 机器。这不是比喻,而是日常工作:
PHP 的第一个优势是它在这个循环里感觉很自然。你不需要在处理请求之前"搭建世界"。
PHP 的默认模型就是面向 Web 的,这一点往往被低估了。
PHP 传统的请求生命周期很简单:
然后下一个请求从头开始。
有人把这当成相对于长驻服务器的劣势,但在实践中它往往是优势:
你也可以用长驻模式跑 PHP(RoadRunner、Swoole 等),它们在特定场景下确实很好。但经典模型对许多 API 仍然是可靠的默认选择,因为它稳定且对运维友好。
即使你在生产环境使用 Laravel 或 Symfony(大多数正经应用确实该用),看看 PHP 为何在 Web 工作中高效还是有帮助的。
declare(strict_types=1);require __DIR__ . '/../vendor/autoload.php';header('Content-Type: application/json; charset=utf-8');$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';function jsonResponse(array $payload, int $status = 200): void {http_response_code($status);echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);exit;}function readJsonBody(): array {$raw = file_get_contents('php://input') ?: '';$data = json_decode($raw, true);return is_array($data) ? $data : [];}if ($method === 'GET' && $path === '/health') {jsonResponse(['ok' => true, 'time' => date(DATE_ATOM)]);}if ($method === 'POST' && $path === '/users') {$body = readJsonBody();$email = strtolower(trim((string)($body['email'] ?? '')));$name = trim((string)($body['name'] ?? ''));if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {jsonResponse(['error' => 'Invalid email'], 422);}if ($name === '') {jsonResponse(['error' => 'Name is required'], 422);}$id = random_int(1000, 9999);jsonResponse(['id' => $id, 'email' => $email, 'name' => $name], 201);}jsonResponse(['error' => 'Not found'], 404);
这不是"最佳实践架构",但它演示了核心思想:PHP 的 Web 循环直接且易懂。这就是基础的生产力优势。
PHP 的 Web 生产力要成为长期优势,前提是保持边界清晰。最容易失去这个优势的方式就是把所有东西都塞进控制器。
一个可扩展的模式:
这是一个与框架无关的小代码例子:
final class CreateUserHandler{public function __construct(private readonly UserService $service) {}public function __invoke(array $body): array{$input = CreateUserInput::fromArray($body);$user = $this->service->create($input);return UserResource::toArray($user);}}
Handler 读起来像一段叙述,这样就对了。
现在服务来做真正的决策:
final class UserService{public function __construct(private readonly UserRepository $users) {}public function create(CreateUserInput $input): User{if ($this->users->existsByEmail($input->email)) {throw new DomainException('Email already registered');}$user = User::register($input->email, $input->name);$this->users->save($user);return $user;}}
这个结构也并不花哨,但它能防止代码库在六个月内变成意大利面条。
PHP 保持实用的一个原因是,做 PHP 后端的团队往往很早就被迫面对 Web 的现实。
不是因为 PHP 特殊,而是因为 Web 本身就不宽容。
如果你调用外部 API 却不设超时、不设重试策略,你就是在埋下未来的事故。
下面是一个用 Guzzle 写的封装,在生产环境中能正经干活:
use GuzzleHttp\Client;use GuzzleHttp\Exception\GuzzleException;final class ShippingClient{private Client $http;public function __construct(string $baseUrl, string $token){$this->http = new Client(['base_uri' => rtrim($baseUrl, '/') . '/','timeout' => 3.0,'connect_timeout' => 1.0,'headers' => ['Authorization' => "Bearer {$token}",'Accept' => 'application/json',],]);}public function createLabel(array $payload): array{$attempts = 0;while (true) {$attempts++;try {$resp = $this->http->post('labels', ['json' => $payload]);$data = json_decode((string)$resp->getBody(), true);return is_array($data) ? $data : [];} catch (GuzzleException $e) {if ($attempts >= 3) {throw new RuntimeException('Shipping API failed after retries', 0, $e);}// small exponential backoff + jitterusleep((int)(100_000 * $attempts) + random_int(0, 50_000));}}}}
当互联网本身是你的依赖时,这种代码是必须的——而 PHP 很适应这个场景。
为什么这是优势一:PHP 契合 HTTP 工作的形状,让团队能快速构建功能而不用与平台对抗。
PHP 的第二个优势是杠杆。
很多语言都能做 Web 开发。但能让"无聊的部分"以可复用的方式被解决、让团队能招到已经熟悉这些模式的人,这样的成熟生态并不多。
当你选择 PHP,你选择的不只是语法,还有:
生态的成熟度能降低风险,风险才是真正要花钱的地方。
Composer 不只是依赖管理——它推动你走向模块化的代码库,用自动加载和命名空间。
一个最小的例子:
{"require": {"php": "^8.2","monolog/monolog": "^3.0","guzzlehttp/guzzle": "^7.0"},"autoload": {"psr-4": {"App\\": "src/"}}}
一旦你采用 PSR-4 自动加载,你的代码就不再是"文件",而开始变成"模块"。这个转变是现代 PHP 比老刻板印象更易维护的重要原因。
框架可能被过度使用,但"不用框架"的做法在应用增长后往往更糟。
Laravel 和 Symfony 为你不想重新发明的东西提供了可靠的默认方案:
大多数生产事故不是来自精妙的业务逻辑,而是来自胶水代码:超时、重试、不一致的校验、部分失败、意外的 payload、不一致的错误响应。
框架默认方案减少这些事故,因为你建立在已经经历过成千上万个生产系统考验的模式之上。
标准在你集成库或者超出最初决策时最为重要。
例如:PSR-3 日志。
use Psr\Log\LoggerInterface;final class BillingService{public function __construct(private readonly LoggerInterface $logger) {}public function charge(int $userId, int $amountCents): void{$this->logger->info('Charge request', ['userId' => $userId,'amountCents' => $amountCents,]);// ...}}
这个类不在乎你今天用 Monolog 还是明天换成别的日志库。这种解耦才能让系统演进。
很多"PHP 黑"来自于对老代码库的体验:弱类型、不一致的模式、"先上线再说"的文化。
现代 PHP 团队通常会用一套简单的质量工具链:
这套组合会改变你写代码的方式。目标不是完美,而是尽早暴露问题,让代码在迭代中保持可读。
下面是一个小例子,静态分析帮你避免一个运行时 bug:
final class UserRepository{public function findByEmail(string $email): User{// DB lookup...return null; // bug}}
配置好静态分析后,这会立即被标记出来。
PHP 的测试也不必很重。一个聚焦的测试可以读起来像文档:
final class MoneyTest extends TestCase{public function testItAddsMoneyInSameCurrency(): void{$a = Money::usd(1000);$b = Money::usd(250);$sum = $a->add($b);$this->assertSame(1250, $sum->cents());$this->assertSame('USD', $sum->currency());}}
为什么这是优势二:PHP 的生态让你能快速交付并安全构建,因为工具链和惯例都已成熟。
如果你做过一段时间后端,你就知道真相:这份工作就是数据转换。
请求进来格式奇怪。数据库行取出来格式奇怪。外部 API 返回的是"差不多是你期望的"。Webhook 在不方便的时候重试。边缘情况在周五发生。
PHP 在这类工作上特别强,因为它在两种模式下都很自如:
为了说明我的意思,来构建一个现实的管道:处理来自支付提供商的 webhook。
这是一个很好的测试,因为它结合了:
数组在边界处没问题,但在整个应用中传递原始数组会变得痛苦。所以:尽早解析,尽早结构化。
final class WebhookEvent{public function __construct(public readonly string $id,public readonly string $type,public readonly int $createdAtEpoch,public readonly array $data) {}public static function fromArray(array $payload): self{return new self(id: (string)($payload['id'] ?? ''),type: (string)($payload['type'] ?? ''),createdAtEpoch: (int)($payload['created_at'] ?? 0),data: is_array($payload['data'] ?? null) ? $payload['data'] : []);}}
这是务实的做法:我们依赖的字段用强类型,原始数据留着灵活性,新字段加进来也不会炸。
final class WebhookSignatureVerifier{public function __construct(private readonly string $secret) {}public function verify(string $rawBody, string $signatureHeader): bool{$expected = hash_hmac('sha256', $rawBody, $this->secret);return hash_equals($expected, $signatureHeader);}}
这里 hash_equals 很重要,用于避免时序攻击。这是个小细节,但这类习惯正是区分业余代码和生产代码的地方。
如果你处理同一个事件两次,你可能会:
所以要存储已处理的事件 ID。
final class WebhookIdempotencyStore{public function __construct(private readonly PDO $pdo) {}public function hasProcessed(string $eventId): bool{$stmt = $this->pdo->prepare("SELECT 1 FROM processed_webhooks WHERE event_id = :id");$stmt->execute([':id' => $eventId]);return (bool)$stmt->fetchColumn();}public function markProcessed(string $eventId): void{$stmt = $this->pdo->prepare("INSERT INTO processed_webhooks (event_id, processed_at)VALUES (:id, NOW())");$stmt->execute([':id' => $eventId]);}}
enum PaymentEventType: string{case PaymentSucceeded = 'payment.succeeded';case PaymentFailed = 'payment.failed';}
步骤五:用事务型 handler 把它们组合起来
final class PaymentWebhookHandler{public function __construct(private readonly WebhookSignatureVerifier $verifier,private readonly WebhookIdempotencyStore $idem,private readonly PaymentService $payments,private readonly PDO $pdo) {}public function handle(string $rawBody, string $signatureHeader): void{if (!$this->verifier->verify($rawBody, $signatureHeader)) {throw new RuntimeException('Invalid webhook signature');}$payload = json_decode($rawBody, true);if (!is_array($payload)) {throw new RuntimeException('Invalid JSON');}$event = WebhookEvent::fromArray($payload);if ($event->id === '' || $event->type === '') {throw new RuntimeException('Missing event fields');}if ($this->idem->hasProcessed($event->id)) {return; // safe no-op}$this->pdo->beginTransaction();try {$this->dispatch($event);$this->idem->markProcessed($event->id);$this->pdo->commit();} catch (Throwable $e) {$this->pdo->rollBack();throw $e;}}private function dispatch(WebhookEvent $event): void{$type = PaymentEventType::tryFrom($event->type);if ($type === null) {// unknown event type: ignore or logreturn;}$paymentId = (string)($event->data['payment_id'] ?? '');if ($paymentId === '') return;match ($type) {PaymentEventType::PaymentSucceeded => $this->payments->markSucceeded($paymentId),PaymentEventType::PaymentFailed => $this->payments->markFailed($paymentId),};}}
这是一个干净的管道:
这就是 PHP 擅长的"Web 现实"代码:处理那些真正重要的脏活,同时保持可读。
为什么这是优势三:大多数后端都是数据管道,而 PHP 在真实世界约束下构建可理解的管道方面很强。
分页是那种看起来简单、等用户翻到深页才暴露问题的功能。它也是个好例子,能说明 PHP 为何还没过时:它同时涉及 HTTP、SQL 性能和响应设计。
Offset 分页(LIMIT 20 OFFSET 100000)迫使数据库扫描并丢弃大量行。在大表上,深页会变慢。
它在并发写入时也可能不一致:插入/删除可能导致"窗口"移动时出现跳过或重复。
游标分页(keyset/seek)通过使用稳定的排序和代表"你上次停在哪里"的游标来避免这些问题。
ORDER BY created_at DESC, id DESC<如果你的数据库支持元组比较:
SELECT id, created_at, total_centsFROM ordersWHERE (created_at, id) < (?, ?)ORDER BY created_at DESC, id DESCLIMIT ?
如果不支持,用显式逻辑:
SELECT id, created_at, total_centsFROM ordersWHERE created_at < ?OR (created_at = ? AND id < ?)ORDER BY created_at DESC, id DESCLIMIT ?
游标通常是一个类似 (created_at, id) 的对,序列化给客户端。Base64 编码在传输时没问题——但不是安全措施。如果你想防止客户端伪造游标,就签名它。
final class Cursor{public function __construct(public readonly string $createdAtIso,public readonly int $id) {}}final class CursorCodec{public function __construct(private readonly string $secret) {}public function encode(Cursor $cursor): string{$json = json_encode(['created_at' => $cursor->createdAtIso,'id' => $cursor->id], JSON_UNESCAPED_SLASHES);$b64 = rtrim(strtr(base64_encode($json), '+/', '-_'), '=');$sig = hash_hmac('sha256', $b64, $this->secret);return $b64 . '.' . $sig;}public function decode(string $token): Cursor{$parts = explode('.', $token, 2);if (count($parts) !== 2) {throw new InvalidArgumentException('Invalid cursor');}[$b64, $sig] = $parts;$expected = hash_hmac('sha256', $b64, $this->secret);if (!hash_equals($expected, $sig)) {throw new InvalidArgumentException('Cursor signature mismatch');}$json = base64_decode(strtr($b64, '-_', '+/'), true);if ($json === false) {throw new InvalidArgumentException('Invalid cursor encoding');}$data = json_decode($json, true);if (!is_array($data)) {throw new InvalidArgumentException('Invalid cursor payload');}return new Cursor(createdAtIso: (string)($data['created_at'] ?? ''),id: (int)($data['id'] ?? 0));}}
final class OrderRepository{public function __construct(private readonly PDO $pdo) {}/*** @return array{items: list, next: ?Cursor} */public function listPage(?Cursor $after, int $limit): array{$limit = max(1, min($limit, 100));if ($after === null) {$sql = "SELECT id, created_at, total_centsFROM ordersORDER BY created_at DESC, id DESCLIMIT :limit";$stmt = $this->pdo->prepare($sql);$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);} else {$sql = "SELECT id, created_at, total_centsFROM ordersWHERE created_at < :created_atOR (created_at = :created_at AND id < :id)ORDER BY created_at DESC, id DESCLIMIT :limit";$stmt = $this->pdo->prepare($sql);$stmt->bindValue(':created_at', $after->createdAtIso);$stmt->bindValue(':id', $after->id, PDO::PARAM_INT);$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);}$stmt->execute();$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);$items = array_map(fn($r) => ['id' => (int)$r['id'],'created_at' => (string)$r['created_at'],'total_cents' => (int)$r['total_cents'],], $rows);$next = null;if ($items !== []) {$last = $items[count($items) - 1];$next = new Cursor($last['created_at'], $last['id']);}return ['items' => $items, 'next' => $next];}}
现在你的 handler 可以解码 after、获取结果、编码 next_cursor——一个横跨 HTTP + SQL + JSON 响应的干净端到端管道。
这就是实践中的最佳平衡点:PHP 的 Web 原生特性、生态工具和数据处理能力在这里汇合。
PHP 不是万能的最佳工具:
但大多数成功的团队不会把这当成重写一切的理由。他们做的是 Web 一直鼓励的事:组合系统。
这不是什么"PHP 用户的自我安慰",这就是正常的系统设计。
回到 Felix 最初之问题——PHP 的三大优势有哪些:
如果你想让 PHP 感觉现代(而不是像那些刻板印象),方法始终如一:
PHP 并不需要追热点。它只要继续做它擅长的事就够了:跑那些实用、好维护、能稳定上线的 Web 系统。
作者:佚名
本篇文章为 @ 场长 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。
请扫描二维码,使用微信支付哦。