在PHP中进行模型验证的最佳方法是什么?

36
我了解到在编程问题中通常有许多种解决方法,每种方法都有其自身的优点和负面影响。今天我想确定在PHP中进行模型验证的最佳方式。以人为例,我概述了我过去使用过的四种不同方法,每种方法包括类和用法示例,以及我对每种方法的喜欢和不喜欢之处。
我的问题是:你认为哪种方法最好?或者你有更好的方法吗?
第一种方法:在模型类中使用setter方法进行验证
好处:
- 简单,只有一个类 - 通过抛出异常,除了业务逻辑(即死亡在出生之前)外,该类永远不会处于无效状态 - 不必记住调用任何验证方法
坏处:
  • 仅能返回1个错误(通过Exception
  • 需要使用异常,并捕获它们,即使错误并不是非常特殊
  • 只能处理一个参数,因为其他参数可能尚未设置(无法比较birth_datedeath_date
  • 模型类可能很长,因为有很多验证
class Person
{
    public $name;
    public $birth_date;
    public $death_date;

    public function set_name($name)
    {
        if (!is_string($name))
        {
            throw new Exception('Not a string.');
        }

        $this->name = $name;
    }

    public function set_birth_date($birth_date)
    {
        if (!is_string($birth_date))
        {
            throw new Exception('Not a string.');
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $birth_date))
        {
            throw new Exception('Not a valid date.');
        }

        $this->birth_date = $birth_date;
    }

    public function set_death_date($death_date)
    {
        if (!is_string($death_date))
        {
            throw new Exception('Not a string.');
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $death_date))
        {
            throw new Exception('Not a valid date.');
        }

        $this->death_date = $death_date;
    }
}

// Usage:

try
{
    $person = new Person();
    $person->set_name('John');
    $person->set_birth_date('1930-01-01');
    $person->set_death_date('2010-06-06');
}
catch (Exception $exception)
{
    // Handle error with $exception
}

方法二:使用模型类中的验证方法进行验证

优点

  • 简单,只需一个类
  • 可以验证(比较)多个参数(因为验证发生在设置所有模型参数之后)
  • 可以通过errors()方法返回多个错误
  • 不会出现异常
  • 保留了其他任务所需的getter和setter方法

缺点

  • 模型可能处于无效状态
  • 开发人员必须记得调用验证is_valid()方法
  • 由于存在大量验证,模型类可能很长
class Person
{
    public $name;
    public $birth_date;
    public $death_date;

    private $errors;

    public function errors()
    {
        return $this->errors;
    }

    public function is_valid()
    {
        $this->validate_name();
        $this->validate_birth_date();
        $this->validate_death_date();

        return count($this->errors) === 0;
    }

    private function validate_name()
    {
        if (!is_string($this->name))
        {
            $this->errors['name'] = 'Not a string.';
        }
    }

    private function validate_birth_date()
    {
        if (!is_string($this->birth_date))
        {
            $this->errors['birth_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->birth_date))
        {
            $this->errors['birth_date'] = 'Not a valid date.';
        }
    }

    private function validate_death_date()
    {
        if (!is_string($this->death_date))
        {
            $this->errors['death_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->death_date))
        {
            $this->errors['death_date'] = 'Not a valid date.';
            break;
        }

        if ($this->death_date < $this->birth_date)
        {
            $this->errors['death_date'] = 'Death cannot occur before birth';
        }
    }
}

// Usage:

$person = new Person();
$person->name = 'John';
$person->birth_date = '1930-01-01';
$person->death_date = '2010-06-06';

if (!$person->is_valid())
{
    // Handle errors with $person->errors()
}

第三种方法:在单独的验证类中进行验证

优点

  • 非常简单的模型(所有验证都在单独的类中完成)
  • 可以验证(比较)多个参数(因为验证发生在设置了所有模型参数之后)
  • 可以通过errors()方法返回多个错误
  • 避免了异常
  • 留下getter和setter方法可用于其他任务

缺点

  • 稍微复杂一些,每个模型需要两个类
  • 模型可能处于无效状态
  • 开发人员必须记得使用验证类
class Person
{
    public $name;
    public $birth_date;
    public $death_date;
}

class Person_Validator
{
    private $person;
    private $errors = array();

    public function __construct(Person $person)
    {
        $this->person = $person;
    }

    public function errors()
    {
        return $this->errors;
    }

    public function is_valid()
    {
        $this->validate_name();
        $this->validate_birth_date();
        $this->validate_death_date();

        return count($this->errors) === 0;
    }

    private function validate_name()
    {
        if (!is_string($this->person->name))
        {
            $this->errors['name'] = 'Not a string.';
        }
    }

    private function validate_birth_date()
    {
        if (!is_string($this->person->birth_date))
        {
            $this->errors['birth_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->person->birth_date))
        {
            $this->errors['birth_date'] = 'Not a valid date.';
        }
    }

    private function validate_death_date()
    {
        if (!is_string($this->person->death_date))
        {
            $this->errors['death_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->person->death_date))
        {
            $this->errors['death_date'] = 'Not a valid date.';
            break;
        }

        if ($this->person->death_date < $this->person->birth_date)
        {
            $this->errors['death_date'] = 'Death cannot occur before birth';
        }
    }
}

// Usage:

$person = new Person();
$person->name = 'John';
$person->birth_date = '1930-01-01';
$person->death_date = '2010-06-06';

$validator = new Person_Validator($person);

if (!$validator->is_valid())
{
    // Handle errors with $validator->errors()
}

方法四:在模型类和验证类中进行验证

优点

  • 通过抛出异常,除了业务逻辑(即死亡发生在出生之前)外,该类永远不会处于无效状态
  • 可以验证(比较)多个参数(因为业务验证发生在设置所有模型参数之后)
  • 可以通过errors()方法返回多个错误
  • 将验证分为两组:类型(模型类)和业务(验证类)
  • 保留了其他任务的getter和setter方法

缺点

  • 错误处理更加复杂,因为有异常抛出(模型类)和错误数组(验证类)
  • 稍微复杂一些,因为每个模型需要两个类
  • 开发人员必须记得使用验证类
class Person
{
    public $name;
    public $birth_date;
    public $death_date;

    private function validate_name()
    {
        if (!is_string($this->person->name))
        {
            $this->errors['name'] = 'Not a string.';
        }
    }

    private function validate_birth_date()
    {
        if (!is_string($this->person->birth_date))
        {
            $this->errors['birth_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->person->birth_date))
        {
            $this->errors['birth_date'] = 'Not a valid date.';          
        }
    }

    private function validate_death_date()
    {
        if (!is_string($this->person->death_date))
        {
            $this->errors['death_date'] = 'Not a string.';
            break;
        }

        if (!preg_match('/(\d{4})-([01]\d)-([0-3]\d)/', $this->person->death_date))
        {
            $this->errors['death_date'] = 'Not a valid date.';
        }
    }
}

class Person_Validator
{
    private $person;
    private $errors = array();

    public function __construct(Person $person)
    {
        $this->person = $person;
    }

    public function errors()
    {
        return $this->errors;
    }

    public function is_valid()
    {
        $this->validate_death_date();

        return count($this->errors) === 0;
    }

    private function validate_death_date()
    {
        if ($this->person->death_date < $this->person->birth_date)
        {
            $this->errors['death_date'] = 'Death cannot occur before birth';
        }
    }
}

// Usage:

try
{
    $person = new Person();
    $person->set_name('John');
    $person->set_birth_date('1930-01-01');
    $person->set_death_date('2010-06-06');

    $validator = new Person_Validator($person);

    if (!$validator->is_valid())
    {
        // Handle errors with $validator->errors()
    }
}
catch (Exception $exception)
{
    // Handle error with $exception
}

1
哇,你实际上本可以在十倍的时间内完成它并继续前进,我猜这不是一个真正的工作情况。 - user557846
12
@ChrisCooney 这是一个很好的帖子,提出了一个非常好的问题。+1 - Vucko
2
@AmazingDreams PHP中的字符串类型提示? - Marko D
1
个人而言,我喜欢第二种方法,可能是因为我真的很喜欢Yii框架,这基本上就是它们的做法。唯一的注意点是你很少需要调用is_valid()validate()或无论你如何称呼它,因为它已经内置在类的beforeSave()部分中了。 - Pitchinnate
显示剩余13条评论
1个回答

2
我认为没有一种最佳方法,这取决于您如何使用类。在这种情况下,当您只有一个简单的数据对象时,我更喜欢使用“方法2:在模型类中使用验证方法进行验证”的方法。
在我看来,坏事情并不是那么糟糕:
模型可能处于无效状态
有时希望能够使模型处于无效状态。
例如,如果您从Web表单填充Person对象并想要记录它。如果您使用第一种方法,您必须扩展Person类,覆盖所有setter以捕获异常,然后您可以使此对象处于无效状态以进行记录。
开发人员必须记得调用验证is_valid()方法
如果模型绝对不能处于无效状态,或者某个方法需要模型处于有效状态,则始终可以从类内部调用is_valid()以确保它处于有效状态。
由于大量验证,模型类可能很长
验证代码仍然必须放在某个地方。大多数编辑器都可以折叠函数,因此在阅读代码时不应该成为问题。如果有什么问题,我认为将所有验证放在一个地方很好。

好的观点,我同意,负面影响并不严重。实际上选项#2和#3是相同的,只是将验证移动到单独的类中...是否有任何好处取决于个人选择。选项#2还比#1多了一个好处,即getter/setter可以用于其他用途。感谢您的回答! - Jonathan
我认为在大多数情况下这并不是一个好的解决方案,因为你把验证部分绑定到了本应“愚蠢”的模型本身。在另一个上下文中,模型可能对“有效性”的定义有所不同。例如,如果您检查某个国家的邮政编码,它可能遵循其他国家的规则。采用此解决方案将使您变得完全不灵活。一个单独的机制来验证模型将解耦这个责任。在我看来,类本身不应该持有任何验证逻辑。 - Jim Panse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接