模型(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 核心职责 数据封装 业务逻辑处理 状态管理 数据库交互 私有字段 带逻辑的访问器 业务方法 规则校验 状态模式 状态机 ORM 手写 SQL Repository 模式

这张图把 Model 的四个核心职责和它们的子项都列出来了。你写代码的时候,可以对照着看看:你的 Model 是不是只做了该做的事?有没有越界?

好了,这一章的内容就到这。Model 层其实还有很多细节,比如领域事件、聚合根、值对象这些 DDD 的概念,咱们后面会慢慢展开。记住一句话:Model 是业务的灵魂,别把它写成数据容器


公众号:蓝海资料掘金营,微信deep3321