模型(Model)深入:模型的责任、数据封装、业务逻辑处理、状态管理以及与数据库的交互方式
好,咱们接着聊模型层。上一章我们把 MVC 的骨架搭起来了,这一章我打算把 Model 这块掰开揉碎了讲清楚。
说实话,很多新手甚至工作两三年的开发者,对 Model 的理解都停留在「Model 就是数据库表映射」这个层面。嗯,这其实是个误区。Model 远不止这么简单。
模型的核心责任:它到底该管什么?
我个人习惯把 Model 比作一个公司的「业务部门」。它不关心数据怎么展示(那是 View 的事),也不关心用户点了哪个按钮(那是 Controller 的事)。它只关心三件事:
- 数据长什么样(数据封装)
- 数据该怎么变(业务逻辑)
- 数据当前是什么状态(状态管理)
你想想看,如果这三件事混在一起,代码会变成什么样?我曾经接手过一个项目,Model 里直接写了 SQL 查询,还在里面调了弹窗提示。那叫一个酸爽。
核心原则:Model 只对自己的数据负责。它不关心数据最终渲染成红色还是蓝色,也不关心用户输入是否合法——那是 Controller 的校验职责。
数据封装:别让数据裸奔
什么叫数据封装?说白了就是给数据穿上衣服,不让外部随便摸。
我见过最典型的反面教材:
// 反面教材:直接暴露字段
class User {
public $name;
public $age;
public $email;
}
$user = new User();
$user->email = 'not-an-email'; // 随便改,没人管
这种写法,我称之为「数据裸奔」。为什么不好?因为外部代码可以随意修改内部数据,没有任何约束。你想想看,如果有一天业务要求邮箱必须包含 @ 符号,你得去所有用到 User 的地方加校验——这不疯了吗?
正确的做法是:
class User {
private $name;
private $age;
private $email;
public function setEmail($email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('邮箱格式不正确');
}
$this->email = $email;
}
public function getEmail() {
return $this->email;
}
}
嗯,这里要注意:封装不是让你写一堆 getter/setter 就完事了。封装的核心是「带逻辑的访问」。setEmail 里加校验,这才是封装的意义。
我的习惯:所有字段默认 private,只有确实需要外部访问时才提供 getter。setter 能不加就不加,尽量通过业务方法来修改数据。
业务逻辑处理:Model 的灵魂
业务逻辑放在哪?这是 MVC 架构里争论最多的问题之一。我个人观点很明确:业务逻辑就应该放在 Model 里。
为什么?因为业务逻辑是「数据的变化规则」。比如「用户下单后库存减一」——这个规则跟数据(库存数量)是强绑定的。你把它放到 Controller 里,换个 Controller 就得重写一遍。
来看个例子:
class Order {
private $items = [];
private $totalPrice = 0;
private $status = 'pending';
public function addItem(Product $product, $quantity) {
// 业务逻辑:检查库存
if ($product->getStock() < $quantity) {
throw new \Exception('库存不足');
}
// 业务逻辑:计算价格
$this->items[] = [
'product' => $product,
'quantity' => $quantity,
'price' => $product->getPrice() * $quantity
];
$this->recalculateTotal();
}
public function pay() {
// 业务逻辑:状态流转
if ($this->status !== 'pending') {
throw new \Exception('订单状态不正确');
}
// 业务逻辑:扣减库存
foreach ($this->items as $item) {
$item['product']->decreaseStock($item['quantity']);
}
$this->status = 'paid';
}
private function recalculateTotal() {
$this->totalPrice = array_sum(array_column($this->items, 'price'));
}
}
你看,addItem 和 pay 这两个方法,包含了完整的业务规则。Controller 只需要调用 $order->pay() 就行,根本不用关心里面怎么扣库存、怎么算总价。
我曾经踩过的坑:把业务逻辑写在 Service 层,Model 只当数据容器。结果 Service 层越来越臃肿,Model 变成了贫血模型。后来重构时,我花了整整两周才把业务逻辑「塞回」Model 里。记住:充血模型才是正道。
状态管理:Model 的「记忆」
Model 是有状态的。一个订单从「待支付」到「已支付」到「已发货」,这就是状态变化。
状态管理最忌讳什么?直接用字符串乱写:
$order->status = 'payed'; // 拼写错误
$order->status = 'shipped'; // 跳过支付直接发货
我建议用状态模式或者枚举来管理:
class OrderStatus {
const PENDING = 'pending';
const PAID = 'paid';
const SHIPPED = 'shipped';
const COMPLETED = 'completed';
const CANCELLED = 'cancelled';
private static $transitions = [
self::PENDING => [self::PAID, self::CANCELLED],
self::PAID => [self::SHIPPED, self::CANCELLED],
self::SHIPPED => [self::COMPLETED],
self::COMPLETED => [],
self::CANCELLED => [],
];
public static function canTransition($from, $to) {
return in_array($to, self::$transitions[$from] ?? []);
}
}
class Order {
private $status;
public function transitionTo($newStatus) {
if (!OrderStatus::canTransition($this->status, $newStatus)) {
throw new \Exception("不能从 {$this->status} 转换到 {$newStatus}");
}
$this->status = $newStatus;
}
}
这样写的好处是:状态流转规则一目了然,想乱改都改不了。
与数据库的交互方式:ORM 还是手写 SQL?
这个问题我经常被问到。我的回答是:看场景。
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| ORM(如 Eloquent、Hibernate) | 开发快、代码简洁、自动映射 | 性能开销大、复杂查询难优化 | CRUD 为主、业务逻辑不复杂 |
| 手写 SQL + 数据映射 | 性能可控、灵活度高 | 开发慢、容易出错 | 报表统计、大数据量、复杂查询 |
| Repository 模式 | 解耦、可测试、切换数据源方便 | 代码量增加、学习成本高 | 大型项目、需要单元测试 |
我个人习惯是:ORM 做基础 CRUD,复杂查询走 Repository。举个例子:
// ORM 方式:简单直接
class UserRepository {
public function findById($id) {
return User::find($id); // Eloquent
}
public function save(User $user) {
$user->save();
}
}
// 手写 SQL:复杂报表
class ReportRepository {
public function getMonthlySales($year, $month) {
$sql = "SELECT
p.category,
SUM(oi.quantity * oi.price) as total
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE YEAR(o.created_at) = ? AND MONTH(o.created_at) = ?
GROUP BY p.category";
return DB::select($sql, [$year, $month]);
}
}
避坑指南:我曾经在一个项目里全部用 ORM,结果有个报表查询跑了 8 秒。后来改成手写 SQL,优化到 0.3 秒。所以别迷信 ORM,该手写时就手写。
知识体系总览
下面这张图,是我对 Model 层核心职责的总结。你可以把它当作一个检查清单:
这张图把 Model 的四个核心职责和它们的子项都列出来了。你写代码的时候,可以对照着看看:你的 Model 是不是只做了该做的事?有没有越界?
好了,这一章的内容就到这。Model 层其实还有很多细节,比如领域事件、聚合根、值对象这些 DDD 的概念,咱们后面会慢慢展开。记住一句话:Model 是业务的灵魂,别把它写成数据容器。