PHP业务验证和表单验证怎么分

wen PHP项目 1

本文目录导读:

PHP业务验证和表单验证怎么分

  1. 核心区别:关注点不同
  2. 经典的错误混用案例
  3. 如何清晰分离(实战架构)
  4. 为什么必须分离?(底层逻辑)
  5. 快速检查清单

在 PHP 开发中,将表单验证业务验证清晰地分开,是构建健壮、可维护应用的关键。

  • 表单验证:关注数据格式基本完整性(“数据是干净的吗?字段存在吗?邮箱格式对吗?”)。
  • 业务验证:关注数据逻辑当前上下文(“这个用户有权限吗?库存够吗?余额足够支付吗?用户名唯一吗?”)。

以下将详细拆解它们的概念、区别以及如何在代码中优雅地分离。


核心区别:关注点不同

维度 表单验证 业务验证
目的 确保数据格式正确,能够安全地进入系统。 确保数据在当前业务规则下是合法、可操作的。
判断依据 类型、长度、格式、必填、枚举值。 数据库状态、用户权限、资源依赖、会计规则、工作流规则。
变化频率 相对稳定,通常只在字段新增或修改时变动。 频繁变化,随业务迭代、促销活动、用户等级、国家法规而变。
数据来源 通常来自 HTTP 请求($_GET$_POST$_FILES)。 来自请求 + 数据库查询结果 + 当前系统状态
执行顺序 优先执行,只有 “干净的” 数据才进入业务验证。 延迟执行,在表单验证通过后,进入 Service/Model 层前执行。
错误提示 “邮箱格式不正确。” “密码长度至少 8 位。” “该用户名已被注册。” “您的账户余额不足。” “该订单已超过可修改时限。”

经典的错误混用案例

很多初学者会把所有逻辑揉在一起:

// 糟糕的混用示例 (在 Controller 中)
public function register(Request $request) {
    // 1. 表单验证 (不错)
    $name = trim($request->input('name'));
    $email = filter_var($request->input('email'), FILTER_VALIDATE_EMAIL);
    if (!$email) { return '邮箱格式错误'; }
    if (strlen($request->input('password')) < 8) { return '密码长度不够'; }
    // 2. 业务验证 (混在一起,查询数据库)
    $user = User::where('email', $email)->first();
    if ($user) { return '该邮箱已被注册'; }  // 这是业务验证
    $user = User::where('name', $name)->first();
    if ($user) { return '该用户名已被注册'; } // 这是业务验证
    // 3. 业务逻辑 (创建用户)
    User::create([...]);
}

问题

  1. 难以测试:测试邮箱格式时,还必须模拟数据库。
  2. 代码臃肿:Controller 逻辑过多,违反单一职责。
  3. 复用困难:CLI 或 API 也需要 “用户名唯一” 校验,需要复制代码。
  4. 可读性差:100 行验证 + 50 行业务逻辑混在一起,其他开发者很难快速判断 “这到底在检查什么?”

如何清晰分离(实战架构)

推荐分层架构(Controller -> Service/Model):

表单验证层:使用专用验证器

常用工具:Laravel FormRequest, Symfony Validator, 或自己封装的 Validator 类。

目标:在请求到达 Controller/Service 之前,确保数据“长得对”。

    • required (必填)
    • email (邮箱格式)
    • alpha_num (字母数字)
    • integer (整数)
    • min:8max:255 (长度限制)
    • in:status_a,status_b (枚举值)
  • 验证通过后:返回清洁后的数据(例如只取允许的字段到 $dto$cleanData)。
// 示例:Laravel FormRequest
class StoreUserRequest extends FormRequest {
    public function rules() {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|max:255',
            'password' => 'required|string|min:8|confirmed',
        ];
    }
}
// Controller 中使用
public function register(StoreUserRequest $request) {
    $validated = $request->validated(); // 此时数据格式已完全验证通过
    // 传递到业务层
    $this->userService->register($validated);
}

业务验证层:在 Service 中使用 Guard / Validator

目标:在执行业务操作前,检查当前上下文是否允许。

    • 唯一性if ($this->userRepo->existsByEmail($email)) throw new UserAlreadyExistsException();
    • 权限if (!$user->can('create_post')) throw new UnauthorizedException();
    • 状态if ($order->status !== OrderStatus::Pending) throw new InvalidOrderStateException();
    • 依赖if ($stock < $quantity) throw new InsufficientStockException();
    • 规则组合if ($this->isPromoActive() && !$this->userIsVip()) throw new PromoNotAvailableException();
  • 推荐模式:使用 Specification Pattern 或者 业务异常

// 示例:Service 层的业务验证
class UserService {
    public function register(array $data): User {
        // 1. 业务验证 (唯一性)
        $this->ensureEmailIsUnique($data['email']);
        $this->ensureNameIsUnique($data['name']);
        // 2. 核心业务逻辑 (创建用户)
        $user = User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);
        // 3. 触发的后续业务 (例如发送欢迎邮件)
        Event::dispatch(new UserRegistered($user));
        return $user;
    }
    private function ensureEmailIsUnique(string $email): void {
        if (User::where('email', $email)->exists()) {
            throw new UserAlreadyExistsException('该邮箱已被注册');
        }
    }
    private function ensureNameIsUnique(string $name): void {
        if (User::where('name', $name)->exists()) {
            throw new UserAlreadyExistsException('该用户名已被占用');
        }
    }
}

Controller 层:只负责协调和响应

class UserController extends Controller {
    public function __construct(private UserService $userService) {}
    public function register(StoreUserRequest $request) {
        try {
            // 表单验证已由 StoreUserRequest 自动完成
            $validated = $request->validated();
            // 执行业务逻辑 (包含业务验证)
            $user = $this->userService->register($validated);
            return response()->json($user, 201);
        } catch (UserAlreadyExistsException $e) {
            // 处理业务异常
            return response()->json(['error' => $e->getMessage()], 409); // 409 Conflict
        } catch (\Exception $e) {
            // 处理其他异常
            return response()->json(['error' => 'Registeration failed'], 500);
        }
    }
}

为什么必须分离?(底层逻辑)

  1. 防御式编程:第一道防线是表单验证(防垃圾数据),第二道防线是业务验证(防逻辑错误),如果表单验证混了业务逻辑,第一道防线一破,第二道防线也形同虚设。
  2. DDD 聚合根:在领域驱动设计中,业务验证是领域模型的责任,表单验证是应用层的职责,把它俩分开,才能让领域模型专注于核心业务规则,而不受 HTTP 输入等细节的干扰。
  3. 审计日志:业务验证失败(如“余额不足”)通常比表单验证失败(如“输入了非法字符”)具有更高的业务价值,需要记录更详细的审计日志,分开后更容易实现。
  4. 错误处理差异
    • 表单验证错误:通常返回 422 Unprocessable Entity + 字段级错误信息。
    • 业务验证错误:通常返回 409 Conflict(资源冲突), 403 Forbidden(权限), 404 Not Found400 Bad Request(逻辑错误)。

快速检查清单

当你在开发时,问自己以下问题:

  • 这个验证需要查数据库吗? → 是 => 业务验证
  • 这个验证会随着促销活动 / 用户等级 / 时间变化吗? → 是 => 业务验证
  • 这个验证只是检查格式 / 长度 / 是否为空吗? → 是 => 表单验证
  • 这个错误是否表示“系统规则不允许”(如余额不足)? → 是 => 业务验证
  • 这个错误是否表示“用户输入有误”(如日期格式错误)? → 是 => 表单验证
验证类型 检查什么 失败示例 执行层 错误 HTTP 状态码
表单验证 格式、基本完整性、必填 “邮箱格式不对”、“密码太短” Controller / Middleware / Request 422
业务验证 逻辑、权限、状态、唯一性、资源可用性 “用户名已存在”、“余额不足”、“库存为 0” Service / Domain Model 409 / 403 / 404 / 400

一句话诀窍把表单验证看作“门卫”,检查证件是否齐全;把业务验证看作“法官”,检查行为是否合法。

抱歉,评论功能暂时关闭!