PHP 项目重构实战:Repository + Service + Controller 三层架构落地指南

为什么你的 PHP 代码越来越难维护?
很多 PHP 项目在初期写得飞快,但随着功能迭代,代码库逐渐变成一个"屎山"——数据库查询散落在各处、业务逻辑和展示层混在一起、测试几乎无从下手。这不是 PHP 语言的问题,而是缺少分层架构的必然结果。
本文将以一个电商订单模块为例,手把手带你把一段典型的"意大利面条"代码重构为清晰的三层架构(Repository + Service + Controller),让你的代码可读、可测、可维护。
第一步:认识问题——原始代码长什么样
先看一段典型的"一把梭"PHP 代码,很多中小项目都长这样:
<?php// OrderController.php(反面教材)class OrderController { public function create() { $db = new PDO('mysql:host=localhost;dbname=shop', 'root', '123456'); $userId = $_POST['user_id']; $productId = $_POST['product_id']; $qty = (int)$_POST['qty']; // 直接查库存 $stmt = $db->prepare("SELECT stock FROM products WHERE id = ?"); $stmt->execute([$productId]); $product = $stmt->fetch(); if ($product['stock'] < $qty) { echo json_encode(['error' => '库存不足']); return; } // 扣库存 $db->prepare("UPDATE products SET stock = stock - ? WHERE id = ?") ->execute([$qty, $productId]); // 写订单 $db->prepare("INSERT INTO orders (user_id, product_id, qty, status) VALUES (?,?,?,'pending')") ->execute([$userId, $productId, $qty]); // 发邮件(直接塞在这里) mail('admin@example.com', '新订单', "用户{$userId}下单了{$qty}个商品{$productId}"); echo json_encode(['success' => true]); }}这段代码有哪些问题?
数据库连接硬编码,密码暴露在代码里
SQL 语句散落在 Controller,无法复用
业务逻辑(库存校验、扣减)和 HTTP 层混在一起
发邮件是副作用,单元测试根本无法隔离
任何改动都可能牵一发而动全身
第二步:分层设计——Repository + Service + Controller
我们采用经典的三层架构:
Repository 层:只负责数据库操作,封装所有 SQL,返回领域对象或数组
Service 层:核心业务逻辑,编排 Repository,处理事务和规则
Controller 层:只负责解析 HTTP 请求、调用 Service、返回响应
目录结构如下:
app/├── Controllers/│ └── OrderController.php├── Services/│ └── OrderService.php├── Repositories/│ ├── OrderRepository.php│ └── ProductRepository.php└── Models/ └── Order.php
第三步:实现 Repository 层
Repository 只做一件事:数据库读写。它不包含任何业务逻辑。
<?php// Repositories/ProductRepository.phpclass ProductRepository { public function __construct(private PDO $db) {} public function findById(int $id): ?array { $stmt = $this->db->prepare("SELECT * FROM products WHERE id = ?"); $stmt->execute([$id]); return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; } public function decrementStock(int $productId, int $qty): void { $this->db->prepare( "UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?" )->execute([$qty, $productId, $qty]); }}// Repositories/OrderRepository.phpclass OrderRepository { public function __construct(private PDO $db) {} public function create(int $userId, int $productId, int $qty): int { $stmt = $this->db->prepare( "INSERT INTO orders (user_id, product_id, qty, status, created_at) VALUES (?,?,?,'pending',NOW())" ); $stmt->execute([$userId, $productId, $qty]); return (int)$this->db->lastInsertId(); }}踩坑记录:decrementStock 中故意在 WHERE 加了 AND stock >= ?,这是一个乐观锁技巧——如果并发场景下库存已被其他请求扣掉,这条 SQL 会影响 0 行,配合 rowCount() 检查可以避免超卖。
第四步:实现 Service 层
Service 层是业务大脑,负责编排 Repository 并处理事务:
<?php// Services/OrderService.phpclass OrderService { public function __construct( private PDO $db, private ProductRepository $productRepo, private OrderRepository $orderRepo, private MailerInterface $mailer // 依赖注入,方便 mock ) {} public function createOrder(int $userId, int $productId, int $qty): int { $product = $this->productRepo->findById($productId); if (!$product) { throw new \RuntimeException('商品不存在'); } if ($product['stock'] < $qty) { throw new \DomainException('库存不足,当前库存:' . $product['stock']); } $this->db->beginTransaction(); try { $this->productRepo->decrementStock($productId, $qty); // 验证扣减是否成功(防超卖) $updated = $this->db->query("SELECT ROW_COUNT()")->fetchColumn(); if ($updated == 0) { throw new \DomainException('库存扣减失败,请重试'); } $orderId = $this->orderRepo->create($userId, $productId, $qty); $this->db->commit(); // 发通知(在事务外,失败不影响下单) $this->mailer->send('admin@example.com', '新订单', "订单#{$orderId}已创建"); return $orderId; } catch (\Exception $e) { $this->db->rollBack(); throw $e; } }}关键设计点:发邮件放在 commit() 之后、事务之外,这样邮件失败不会导致订单回滚。
第五步:精简 Controller
重构后的 Controller 只做三件事:解析请求、调用 Service、返回响应。
<?php// Controllers/OrderController.phpclass OrderController { public function __construct(private OrderService $orderService) {} public function create(Request $request): Response { try { $orderId = $this->orderService->createOrder( userId: (int)$request->input('user_id'), productId: (int)$request->input('product_id'), qty: (int)$request->input('qty') ); return Response::json(['order_id' => $orderId], 201); } catch (\DomainException $e) { return Response::json(['error' => $e->getMessage()], 422); } catch (\Exception $e) { // 记录日志,不暴露内部错误 logger()->error('下单失败', ['exception' => $e]); return Response::json(['error' => '服务异常,请稍后重试'], 500); } }}注意异常处理的分层:DomainException 是业务异常,直接告知用户;其他未知异常记录日志后返回通用错误,绝不暴露内部堆栈给客户端。
重构前后对比与总结
经过这次重构,代码的可维护性有了质的提升。以下是直观对比:
可测试性:Service 层通过依赖注入,单元测试时可以 mock Repository 和 Mailer,无需真实数据库
可复用性:
ProductRepository::findById可以被多个 Service 复用,不再重复写 SQL可读性:每个类职责单一,新人接手代码时一眼知道去哪里找什么
防超卖:通过乐观锁 + ROW_COUNT 双重保障,解决了原始代码中的并发超卖问题
这套架构不依赖任何框架,在原生 PHP 项目或 Laravel、Symfony 等框架中均可应用。下一步可以继续引入 DTO(Data Transfer Object)来规范数据传递,或者配合 PHP 8.1 的枚举类型来强化订单状态管理。
好的架构不是一步到位的,但每一次小重构都在让代码变得更健康。
发布评论
热门评论区: