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负责与数据库的所有交互。那么如何使模型从内部使用数据库呢?

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

如果User可以自我验证,它应该能够孤立地这样做;它的验证不应该涉及任何外部交互。

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

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

保持User类不变,它本身就是一个好公民。

我会将该方法设为私有isEmailUnique(如果它仅用于此目的(,并通过addUser中是否存在带有该电子邮件的User进行检查。另一方面,这会将逻辑的责任拉到DAO(参见:服务和DAO层的责任和使用(

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

解决此问题

的一种方法是完全删除方法User::isValid,以支持在其构造函数中传递它所需的所有内容,从那里运行验证:

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

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

确保这一点的更好方法是使用封装此验证逻辑的 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类简单地保留为实体类,并将所有其他内容放在其他类中 - 否则它将增长并增长到无法再维护的状态:)

另请检查:https://en.wikipedia.org/wiki/Single_responsibility_principle

我认为您可以使用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并在构造函数中初始化它。并在模型类中实现所有添加/编辑/删除操作的方法。

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