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

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_types=1);
  • 标量类型和返回类型
  • 类型化属性
  • 联合类型
  • 枚举
  • 属性注解(Attributes)
  • 更好的错误语义
  • Composer 成为标配
  • PSR 标准
  • 优秀的框架(Laravel、Symfony)和组件
  • 静态分析工具(PHPStan/Psalm)
  • 代码格式化工具(PHP-CS-Fixer)
  • 容器化 / CI 工作流

语言进化了,但很多团队没有。

所以真正的问题是:你写 PHP 的时候,是把它当成一门现代后端语言,还是当成赶工时凑合用的脚本?

经典 PHP 反模式:"什么都塞进控制器"

下面这套流程,在很多项目里都能看到:

  1. 控制器接收请求
  2. 控制器做验证
  3. 控制器拼查询
  4. 控制器处理业务规则
  5. 控制器更新数据库
  6. 控制器格式化响应
  7. 控制器触发副作用(邮件、队列)

能跑,能上线,功能还能往上堆。然后就开始变脆------因为控制器已经变成了一个揽了业务规则、数据持久化和 I/O 的上帝对象。

看一个简化版的例子。

❌ 反模式:所有逻辑塞在控制器里

php 复制代码
<?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 复制代码
<?php
declare(strict_types=1);
final class PlaceOrderCommand
{
    /**
     * @param array<int, array{productId:int, qty:int}> $items
     */
    public function __construct(
        public readonly int $userId,
        public readonly array $items
    ) {}
}

Step B:定义领域异常(业务错误不应该是 500)

php 复制代码
<?php
declare(strict_types=1);
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 复制代码
<?php
declare(strict_types=1);
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 array<int, 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 复制代码
<?php
declare(strict_types=1);
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 复制代码
<?php
declare(strict_types=1);
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,没用什么黑魔法。

测试:停止甩锅给语言的最快方式

很多 PHP 团队不写测试,因为早年写起来确实别扭。但现在的 PHP 写测试已经很顺手了。

下面用一个简单的 PHPUnit 例子演示:通过 mock 仓储测试业务逻辑,完全不需要数据库。

✅ PHPUnit 风格的单元测试(无需数据库)

php 复制代码
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class PlaceOrderHandlerTest extends TestCase
{
    public function test_it_places_an_order_and_returns_total(): void
    {
        $tx = new class implements TransactionManager {
            public function run(callable $fn): mixed { return $fn(); }
        };
        $users = new class implements UserRepository {
            public function getStatus(int $userId): ?string { return 'active'; }
        };
        $products = new class implements ProductRepository {
            private array $stock = [10 => 5];
            public function getSnapshot(int $productId): ?ProductSnapshot {
                if ($productId !== 10) return null;
                return new ProductSnapshot(10, price: 200, stock: $this->stock[10]);
            }
            public function decreaseStock(int $productId, int $qty): void {
                $this->stock[$productId] -= $qty;
            }
        };
        $orders = new class implements OrderRepository {
            public function create(int $userId, int $total, array $lines): int { return 123; }
        };
        $handler = new PlaceOrderHandler($tx, $users, $products, $orders);
        $cmd = new PlaceOrderCommand(
            userId: 7,
            items: [
                ['productId' => 10, 'qty' => 2],
            ]
        );
        $result = $handler->handle($cmd);
        $this->assertSame(123, $result->orderId);
        $this->assertSame(400, $result->total);
    }
}

如果你的用例能这样测试,"PHP 不可维护"这种话就很难再理直气壮地说出口了。可维护性不是语言自带的能力,是靠结构和测试撑起来的。

"框架神话":Laravel/Symfony 不会自动拯救你

框架有用,但它拦不住你写出烂架构。比如在 Laravel 里,你照样可以:

  • 写出臃肿的控制器
  • 把领域逻辑全塞进 Eloquent 模型
  • 因为觉得"绕了一层"而直接跳过服务层

如果你见过控制器写了 800 行的 Laravel 项目,问题不在 Laravel,而在于团队把框架当成了不做设计的理由。

框架是工具箱。它能盖房子------也能堆一堆木头。

为什么 PHP 比其他技术栈更容易挨骂

原因说起来有点微妙:在 PHP 里,糟糕的决策暴露得更快。

有些技术栈的抽象层能把烂代码盖住更长时间。但在 PHP 里,写得烂一眼就能看出来:

  • 职责混杂
  • 约定不一致
  • 复制粘贴的重复代码
  • 隐蔽的全局变量
  • 随意的错误处理

PHP 不会替你兜底,所以它最容易被拎出来当靶子。

但一门逼你守规矩的语言不见得"更好",它只是让你更难偷懒。PHP 把门槛放得很低------所以你的团队文化反而更重要。

"整洁的 PHP"很无聊------这是夸奖

写得整洁的 PHP 代码,通常看起来平平无奇:

  • 命名清晰
  • 函数短小
  • 输入输出明确
  • 错误处理可预测
  • 依赖注入
  • 尽量少的魔法

它不炫技,不追求巧妙。

无聊的代码在凌晨两点容易调试。无聊的代码容易交给新同事。无聊的代码在团队扩张时能扛得住。

如果你希望 PHP 不再被当笑话,那就写无聊的 PHP。

实操清单:如何停止写"被人骂"的 PHP

如果你想写出不被人嘲笑的 PHP 系统,下面是一份实用的底线清单:

  • 在新代码中开启严格类型declare(strict_types=1);
  • 所有依赖通过 Composer 和自动加载管理,不要手动 include。
  • 保持控制器轻薄(只处理 HTTP),业务规则放到 handler/service 中。
  • 领域规则和持久化分离,仓储或查询服务能保持数据访问的一致性。
  • 使用显式的 DTO 传递请求和命令,不要到处传裸数组。
  • 区分领域错误和系统错误,不是每个异常都该是 500。
  • 在用例层添加单元测试,如果业务逻辑离了数据库就没法测,说明你的边界划错了。
  • 使用静态分析工具(PHPStan/Psalm)防止隐性回归。
  • 引入代码风格工具并在 CI 中强制执行,一致性很重要。
  • 持续重构,如果重构变成了"一个项目",那它永远不会发生。

以上这些不是 PHP 独有的------恰恰相反,放到哪个语言都一样。

结语:PHP 是一面镜子------别砸镜子

PHP 不完美,没有语言是完美的。但用 PHP 遇到的大部分痛苦不是语言造成的,而是写法造成的:赶工、职责混杂、边界模糊、"以后再改"的文化。

一个 PHP 项目变得不可收拾的时候,很少是因为 PHP 撑不起好架构,而是从来没人把架构当回事。

PHP 是一面镜子:

  • 如果你有纪律,它看起来很专业。
  • 如果你很随意,它看起来很混乱。
  • 如果你在持续的赶工压力下没有质量标准,它看起来就像战场。

所以下次有人说"都怪 PHP"的时候,你可以反问一句:

是 PHP 的问题......还是我们流程的问题?

因为代码不是语言自己写的。

是你写的。

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

相关推荐
zdl6867 小时前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端
回到原点的码农7 小时前
Spring Boot实时推送技术详解:三个经典案例
spring boot·后端·状态模式
mldlds7 小时前
Spring Boot 集成 Kettle
java·spring boot·后端
同聘云7 小时前
阿里云国际站服务器cdn网络故障的解决方法是什么?
服务器·开发语言·阿里云·php
zopple15 小时前
常见的 Spring 项目目录结构
java·后端·spring
cjy00011116 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
wefly201717 小时前
从使用到原理,深度解析m3u8live.cn—— 基于 HLS.js 的 M3U8 在线播放器实现
java·开发语言·前端·javascript·ecmascript·php·m3u8
小江的记录本17 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji341617 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
luanma15098017 小时前
PHP vs C++:编程语言终极对决
开发语言·c++·php