PHP 中的模型验证需要与数据库交互

Model validation in PHP that requires interaction with the database

假设我有这个模型。 (出于演示目的,我把它做得非常简单。)

class User
{
    public $id;
    public $email;
    public $password;
    public $errors = [];

    public function isValid()
    {
        if (strpos($this->email, '@') === false) {
            $this->errors['email'] = 'Please enter an email address';
        }
        // ...

        return !$this->errors;
    }
}

假设我有这个 DAO 用于检索、添加、更新和删除用户。

class UserDAO
{
    public function getUsers() { ... }

    public function getUserById($id) { ... }

    public function addUser(User $user) { ... }

    public function updateUser(User $user) { ... }

    public function deleteUser($id) { ... }

    public function isEmailUnique($email) { ... }
}

当我处理表格时,我通常会这样做:

$userDAO = new UserDAO();
$user = new User();
$user->email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$user->password = filter_input(INPUT_POST, 'password');

if ($user->isValid()) {
    if ($userDAO->addUser($user)) {
        // ...
    } else {
        // ...
    }
} else {
    // do something with $user->errors
}

现在,假设我的用户验证的一部分应该是检查电子邮件是否是唯一的,我如何使它成为用户模型的一部分?那么,当调用 $user->isValid() 时,它还会检查电子邮件是否唯一?还是我做错了?

根据我对 DAO 的薄弱理解,DAO 负责与数据库的所有交互。那么如何使模型从内部与数据库一起工作?

我认为在这种情况下,验证是应用程序逻辑的一部分,因为您需要未存储在模型中的数据。所以在不同的控制器函数中实现验证逻辑会更好。

此外,已经有一个类似的问题和类似的答案:Best Place for Validation in Model/View/Controller Model?

UserDAO class 必须实现一个名为 userExists 的方法。此方法仅检查电子邮件地址是否已存在。它在 BD 中检查它,因此它的位置在 UserDAO class 中。它必须是私有方法,addUser 使用它来 return 一个正确的值或 false/null

保持User class原样,它本身就是一个好公民。

我会让方法 isEmailUnique private (如果它仅用于此目的) 并检查是否存在User 中包含该电子邮件的 addUser。另一方面,这会将逻辑的责任拉到 DAO(参见:Responsibilities and use of Service and DAO Layers

因此,如果您更改 isValid 的行为以检查用户是否已经访问数据库,您将破坏您的设计。

我认为你可以使用 DAO 作为验证函数的参数。

public function isValid($dao)
{
    if (strpos($this->email, '@') === false) {
        $this->errors['email'] = 'Please enter an email address';
    }
    if ($dao->isEmailUnique($this->email) === false) {
        $this->errors['email'] = 'Email address should be unique';
    }
    // ...

    return !$this->errors;
}

但可能更好的方法是在用户模型中使用 DAO。添加到模型私有变量 $dao 并在构造函数中初始化它。并在模型 class.

中实现 add/edit/delete 操作的所有方法

解决此问题的一种方法是完全删除方法 User::isValid,以便在其构造函数中传递它需要的所有内容,运行 从那里进行验证:

class User
{
    public function __construct($email) {
        if (strpos($email, '@') === false) {
            throw new \InvalidArgumentException("Invalid email");
        }

        $this->email = $email;
    }
}

如果您考虑一下,是什么让用户有效?如果这是一个有效的电子邮件地址,请确保在构造用户对象时传入一个。这使您的用户对象始终有效。

确保这一点的更好方法是使用封装此验证逻辑的 ValueObject,以便您可以在其他对象中使用它,避免大量冗余和样板代码:

class Email
{
    public function __construct($email)
    {
        if (strpos($email, '@') === false) {
            throw new \InvalidArgumentException("Invalid email");
        }

        $this->email = $email;
    }
}

class User
{
    public function __construct(Email $email)
    {
        $this->email = $email;
    }
}

class ProspectiveUser
{
    public function __construct(Email $email)
    {
        $this->email = $email;
    }   
}

现在,就使用数据库验证用户而言,您可以将其完美地封装在您的 DAO 中。 DAO 可以执行检查以确保用户不在数据库中,让 DAO 消费者不知道它,除了当用户已经存在于数据库中时它应该知道如何处理错误的情况:

class UserDAO
{
    public function recordNewUser(User $user)
    {
        if ($this->userExists()) {
            throw new UserAlreadyExistsException();
        }

        $this->persist($user);
        $this->flush($user);
    }

    private function userExists(User $user)
    {
        $user = $this->findBy(['email' => $user->getEmail()]);

        return !is_null($user);
    }
}

如您所见,DAO 为您提供了一个用于保存新用户的接口,但是如果不满足电子邮件唯一性的约束,该操作可能会失败。

我的建议是:验证您的 User 模型时不要考虑电子邮件地址的唯一性。唯一性是一个 UserDAO 问题,而不是 User 问题。

如果 User 可以自我验证,它应该能够在孤立的情况下进行;它的验证不应与任何外部交互有关。

电子邮件地址是否唯一重要的唯一时间是在您尝试将其插入数据库的那一刻。考虑到多个并发用户的可能性,理论上可以验证地址的唯一性,并在您尝试插入时不再是唯一的。

我认为最直接可靠的方法是在您的数据库中为电子邮件地址添加一个唯一约束,然后在您的 addUser() 方法中,只需 try 添加它。如果您的数据库告诉您它不是唯一的,那么您就知道它不是唯一的。你不可能真的事先知道。

我会将所有验证问题从 User class 中移出并移至控制器层(例如可以调用 UserDAO 来检查电子邮件的唯一性)。最好将 User class 简单地作为一个实体 class 并将所有其他内容放在其他 classes 中 - 否则它会增长并增长到现在的状态不再可维护:)

同时检查:https://en.wikipedia.org/wiki/Single_responsibility_principle