PHP 的问题不在语言本身,而在我们怎么写它

📅 2026/6/30 1:57:47
PHP 的问题不在语言本身,而在我们怎么写它
PHP 的口碑几乎在每次技术讨论中都会被拎出来。应用慢、乱、不安全、改起来痛苦总有人耸耸肩说嗯……毕竟是 PHP 嘛。这话很少出于技术判断更像是一种习惯性甩锅。事实比这简单也更扎心大多数 PHP 系统之所以难维护是我们自己放任的结果。PHP 不会一上来就逼你做架构设计、划边界、守规矩。它很宽容很务实特别擅长让你把一个“能跑就行”的东西赶出来。但今天能跑的代码库明天可能就是灾难。一个 PHP 项目沦为恐怖故事很少是因为 PHP 做不到更好而是团队从来没养成那些能让项目越做越大还不崩的习惯——结构、测试、约定、关注点分离。现代 PHP 完全有能力做到严格类型是的真正的类型整洁架构依赖注入表达力强的领域模型规范的错误处理可靠的测试高性能OPcache/JIT、缓存、合理的 I/O成熟的工具链如果你对 PHP 的印象还停留在到处 include 文件和在视图里写 SQL那你骂的不是 PHP 这门语言而是一种早该被淘汰的 PHP 写法。这篇文章不是在给 PHP 洗地只是想说清楚一件事PHP 是一面镜子照出来的是你的工程文化。照出来不好看换面镜子也没用。PHP 很宽容——宽容的语言会放大你的习惯有些语言生态从一开始就逼你把结构搭好。想做稍微复杂一点的东西就绕不开包、模块、接口、依赖注入这些概念哪怕你没主动要求约束也自动就在那了。PHP 的玩法不一样可以从一个文件起步可以毫无阻力地混合各层可以在任何地方访问全局变量可以在控制器里直接查数据库可以忽略类型照样上线这种灵活性本身不是坏事PHP 靠它当了多年 Web 开发的默认选择。但它也埋了一个坑结构显得可有可无而可有可无的东西在赶工时一定会被砍掉。很多“PHP 太烂了”的故事背后的真实剧情是“赶工期上了线然后重构的债一直没还”。PHP 没有造成这个问题它只是没有阻止。都怪 PHP往往是在逃避责任系统让人痛苦的时候甩锅给语言最省事因为语言最容易看到。真正的原因往往藏得更深没有统一的编码规范没有架构负责人没有测试没有为重构分配时间代码评审时松时紧先交付再说的激励机制这些问题哪个技术栈都有。区别在于 PHP 能让你在几乎没有约束的情况下把项目推得很远技术债悄悄攒着——然后在某一天集中爆发。PHP 成了替罪羊因为承认流程烂了比甩锅给语言难多了。现代 PHP 不是你记忆中的 PHP如果你对 PHP 的认知还停在PHP 5 加一堆随意 include的年代那你错过的东西太多了declare(strict_types1);标量类型和返回类型类型化属性联合类型枚举属性注解Attributes更好的错误语义Composer 成为标配PSR 标准优秀的框架Laravel、Symfony和组件静态分析工具PHPStan/Psalm代码格式化工具PHP-CS-Fixer容器化 / CI 工作流语言进化了但很多团队没有。所以真正的问题是你写 PHP 的时候是把它当成一门现代后端语言还是当成赶工时凑合用的脚本经典 PHP 反模式什么都塞进控制器下面这套流程在很多项目里都能看到控制器接收请求控制器做验证控制器拼查询控制器处理业务规则控制器更新数据库控制器格式化响应控制器触发副作用邮件、队列能跑能上线功能还能往上堆。然后就开始变脆——因为控制器已经变成了一个揽了业务规则、数据持久化和 I/O 的上帝对象。看一个简化版的例子。❌ 反模式所有逻辑塞在控制器里?php class CheckoutController { public function placeOrder(array $request): array { $userId (int)($request[user_id] ?? 0); $items $request[items] ?? []; if ($userId 0 || empty($items)) { return [ok false, error Invalid request]; } $pdo new PDO($_ENV[DB_DSN], $_ENV[DB_USER], $_ENV[DB_PASS]); $pdo-beginTransaction(); try { // Load user $stmt $pdo-prepare(SELECT id, status FROM users WHERE id ?); $stmt-execute([$userId]); $user $stmt-fetch(PDO::FETCH_ASSOC); if (!$user || $user[status] ! active) { throw new RuntimeException(User not active); } // Calculate total $total 0; foreach ($items as $it) { $productId (int)$it[product_id]; $qty (int)$it[qty]; $stmt $pdo-prepare(SELECT id, price, stock FROM products WHERE id ?); $stmt-execute([$productId]); $product $stmt-fetch(PDO::FETCH_ASSOC); if (!$product) { throw new RuntimeException(Product not found); } if ($qty 0 || $qty (int)$product[stock]) { throw new RuntimeException(Insufficient stock); } $total ((int)$product[price]) * $qty; // Reduce stock inline $stmt $pdo-prepare(UPDATE products SET stock stock - ? WHERE id ?); $stmt-execute([$qty, $productId]); } // Insert order $stmt $pdo-prepare(INSERT INTO orders(user_id, total, created_at) VALUES(?, ?, NOW())); $stmt-execute([$userId, $total]); $orderId (int)$pdo-lastInsertId(); // Insert items $stmt $pdo-prepare(INSERT INTO order_items(order_id, product_id, qty) VALUES(?, ?, ?)); foreach ($items as $it) { $stmt-execute([$orderId, (int)$it[product_id], (int)$it[qty]]); } $pdo-commit(); return [ok true, order_id $orderId, total $total]; } catch (Throwable $e) { $pdo-rollBack(); return [ok false, error $e-getMessage()]; } } }这段代码烂不是因为它用 PHP 写的而是因为它把这些东西全搅在了一起输入验证事务管理业务规则持久化状态变更响应格式化不连数据库就没法测业务逻辑不复制代码就没法复用规则改一个小地方都提心吊胆。如果你平时见到的 PHP 都长这样有偏见很正常。但话说回来PHP 没逼你写成这样——我们自己选的这条路图的就是快。好的 PHP长什么样无聊的结构清晰的边界写得好的 PHP 代码往往看起来没什么技术含量。这不是坏事——无聊的代码就是可预测的代码。更合理的分层方式是控制器只处理 HTTP 层请求/响应应用/服务层协调用例领域对象负责维护业务不变量仓储层处理持久化副作用通过接口隔离下面用更清晰的结构重写同一个功能。✅ 现代 PHP 用例风格下面的代码尽量精简——不绑定特定框架但和 Laravel/Symfony 的写法兼容。Step A定义请求 DTO?php declare(strict_types1); final class PlaceOrderCommand { /** * param arrayint, array{productId:int, qty:int} $items */ public function __construct( public readonly int $userId, public readonly array $items ) {} }Step B定义领域异常业务错误不应该是 500?php declare(strict_types1); class DomainException extends RuntimeException {} final class UserNotActive extends DomainException {} final class ProductNotFound extends DomainException {} final class InsufficientStock extends DomainException {} final class InvalidOrder extends DomainException {}Step C为依赖定义小接口?php declare(strict_types1); interface UserRepository { public function getStatus(int $userId): ?string; } final class ProductSnapshot { public function __construct( public readonly int $id, public readonly int $price, public readonly int $stock ) {} } interface ProductRepository { public function getSnapshot(int $productId): ?ProductSnapshot; public function decreaseStock(int $productId, int $qty): void; } final class OrderResult { public function __construct( public readonly int $orderId, public readonly int $total ) {} } interface OrderRepository { /** * param arrayint, array{productId:int, qty:int, price:int} $lines */ public function create(int $userId, int $total, array $lines): int; } interface TransactionManager { /** * template T * param callable():T $fn * return T */ public function run(callable $fn): mixed; }Step D实现用例服务层?php declare(strict_types1); final class PlaceOrderHandler { public function __construct( private readonly TransactionManager $tx, private readonly UserRepository $users, private readonly ProductRepository $products, private readonly OrderRepository $orders ) {} public function handle(PlaceOrderCommand $cmd): OrderResult { if ($cmd-userId 0 || $cmd-items []) { throw new InvalidOrder(User and items are required.); } $status $this-users-getStatus($cmd-userId); if ($status ! active) { throw new UserNotActive(User is not active.); } return $this-tx-run(function () use ($cmd): OrderResult { $lines []; $total 0; foreach ($cmd-items as $item) { $productId $item[productId]; $qty $item[qty]; if ($qty 0) { throw new InvalidOrder(Quantity must be 0.); } $snapshot $this-products-getSnapshot($productId); if (!$snapshot) { throw new ProductNotFound(Product {$productId} not found.); } if ($qty $snapshot-stock) { throw new InsufficientStock(Insufficient stock for {$productId}.); } $lineTotal $snapshot-price * $qty; $total $lineTotal; // Reserve/update stock $this-products-decreaseStock($productId, $qty); $lines[] [ productId $productId, qty $qty, price $snapshot-price, ]; } $orderId $this-orders-create($cmd-userId, $total, $lines); return new OrderResult($orderId, $total); }); } }Step E控制器变得轻薄且可测试?php declare(strict_types1); final class CheckoutController { public function __construct(private readonly PlaceOrderHandler $handler) {} public function placeOrder(array $request): array { try { $itemsRaw $request[items] ?? []; $items array_map( fn($it) [ productId (int)($it[product_id] ?? 0), qty (int)($it[qty] ?? 0), ], is_array($itemsRaw) ? $itemsRaw : [] ); $cmd new PlaceOrderCommand( userId: (int)($request[user_id] ?? 0), items: $items ); $result $this-handler-handle($cmd); return [ ok true, order_id $result-orderId, total $result-total, ]; } catch (DomainException $e) { return [ok false, error $e-getMessage()]; } catch (Throwable $e) { // avoid leaking internals return [ok false, error Unexpected error]; } } }这个版本不是为了复杂而复杂而是把复杂度放到了该放的地方业务规则集中管理事务受控控制器极简依赖抽象化终于可以写测试了而且这些全是原生 PHP没用什么黑魔法。