Data Mapper模式,异常和处理用户提供的数据

时间:2014-05-09 16:15:05

标签: php exception-handling datamapper domain-model

在应用Data Mapper模式时,模型(在我的情况下为Domain Model)尽可能负责业务逻辑,而不是将实体保存到数据库的映射器。

在模型之外构建一个单独的业务逻辑验证器来处理用户提供的数据似乎是否合理?

以下是PHP语法中的一个例子。

假设我们有一个实体$person。假设该实体的属性surname在保存时不能为空。

用户输入了surname的非法空值。由于模型负责封装业务逻辑,因此当用户输入的$person->surname = $surname;为空字符串时,我希望$surname以某种方式表示操作不成功。

在我看来,如果我们尝试使用非法值填充其中一个属性,$person应该抛出异常。

然而,from what I've read on exceptions“用户输入'错误'输入也不例外:这是预期的。”其含义是不依赖异常来验证用户数据。

您如何建议解决此问题,在让域模型定义业务逻辑之间取得平衡,而不是依赖于域模型在填写用户输入数据时抛出的异常?

4 个答案:

答案 0 :(得分:5)

域模型不一定是可以直接转换为数据库行的对象。 您的Person示例符合此描述,我喜欢将此类对象称为实体(从Doctrine 2 ORM中采用)。 但是,正如Martin Fowler所描述的那样,域模型是包含行为和数据的任何对象。

严格的解决方案

这是您所描述的问题的一个非常严格的解决方案:

假设您的Person域模型(或实体)必须具有名字和姓氏,并且可选一个婚前姓名。这些必须是字符串,但为简单起见,可能包含任何字符。 您希望强制执行此类Person存在时,这些先决条件得到满足。这个课程看起来像这样:

class Person
{
    /**
     * @var string
     */
    protected $firstname;

    /**
     * @var string
     */
    protected $lastname;

    /**
     * @var string|null
     */
    protected $maidenname;

    /**
     * @param  string      $firstname
     * @param  string      $lastname
     * @param  string|null $maidenname
     */
    public function __construct($firstname, $lastname, $maidenname = null)
    {
        $this->setFirstname($firstname);
        $this->setLastname($lastname);
        $this->setMaidenname($maidenname);
    }

    /**
     * @param string $firstname
     */
    public function setFirstname($firstname)
    {
        if (!is_string($firstname)) {
            throw new InvalidArgumentException('Must be a string');
        }

        $this->firstname = $firstname;
    }

    /**
     * @return string
     */
    public function getFirstname()
    {
        return $this->firstname;
    }

    /**
     * @param string $lastname
     */
    public function setLastname($lastname)
    {
        if (!is_string($lastname)) {
            throw new InvalidArgumentException('Must be a string');
        }

        $this->lastname = $lastname;
    }

    /**
     * @return string
     */
    public function getLastname()
    {
        return $this->lastname;
    }

    /**
     * @param string|null $maidenname
     */
    public function setMaidenname($maidenname)
    {
        if (!is_string($maidenname) or !is_null($maidenname)) {
            throw new InvalidArgumentException('Must be a string or null');
        }

        $this->maidenname = $maidenname;
    }

    /**
     * @return string|null
     */
    public function getMaidenname()
    {
        return $this->maidenname;
    }
}

正如您所看到的,没有办法(无视Reflection)您可以在不满足先决条件的情况下实例化Person对象。 这是一件好事,因为无论何时遇到Person对象,您都可以100%确定您正在处理的数据类型。

现在您需要第二个域模型来处理用户输入,我们可以将其称为PersonForm(因为它通常代表在网站上填写的表单)。 它具有与Person相同的属性,但盲目接受任何类型的数据。 它还有一个验证规则列表,一个使用这些规则验证数据的isValid()方法,以及一个获取任何违规的方法。 我会把课程的定义留给你的想象:)

最后,您需要一个控制器(或服务)将这些绑定在一起。这是一些伪代码:

class PersonController
{
    /**
     * @param Request      $request
     * @param PersonMapper $mapper
     * @param ViewRenderer $view
     */
    public function createAction($request, $mapper, $view)
    {
        if ($request->isPost()) {
            $data = $request->getPostData();

            $personForm = new PersonForm();
            $personForm->setData($data);

            if ($personForm->isValid()) {
                $person = new Person(
                    $personForm->getFirstname(),
                    $personForm->getLastname(),
                    $personForm->getMaidenname()
                );

                $mapper->insert($person);

                // redirect
            } else {
                $view->setErrors($personForm->getViolations());
                $view->setData($data);
            }
        }

        $view->render('create/add');
    }
}

如您所见,PersonForm用于拦截和验证用户输入。只有当该输入有效时,才会创建Person并将其保存在数据库中。

业务规则

这确实意味着某些业务逻辑将被复​​制:

Person中,您希望强制执行业务规则,但是当某些内容关闭时,它可以简单地抛出异常。

PersonForm中,您将拥有应用相同规则的验证程序,以防止无效的用户输入到达Person。但是这些验证器可以更先进。想想人类错误消息,破坏第一条规则等等。你也可以应用稍微改变输入的过滤器(例如小写用户名)。

换句话说:Person将在较低级别强制执行业务规则,而PersonForm则更多地是关于处理用户输入。

更方便

不太严格的方法,但可能更方便:

限制在Person中完成的验证以强制执行所需的属性,并强制执行属性类型(字符串,整数等)。仅此而已。

您还可以在Person中找到约束列表。这些是业务规则,但没有实际验证代码。所以它只是一些配置。

拥有能够接收数据和约束列表的Validator服务。它应该能够根据约束验证数据。对于每种类型的约束,您可能需要一个小的验证器类。 (看看Symfony 2 validator component)。

PersonForm可以注入Validator服务,因此可以使用该服务验证用户输入。

最后,拥有PersonManager服务,负责您要对Person执行的任何操作(例如创建/更新/删除,以及注册/激活等等) )。 PersonManager将需要PersonMapper作为依赖项。

当您需要创建Person时,可以调用类似$personManager->create($userInput);的内容。该调用将创建PersonForm,验证数据,创建Person(当数据时)是有效的),并使用Person保留PersonMapper

关键在于:

您可以围绕所有这些类画一个圆圈,并将其称为您的" Person域" (DDD)。并且该域的接口(入口点)是PersonManager服务。 您希望在Person 上执行的每项操作必须通过PersonManager

如果你坚持在你的应用程序中,你应该确保业务规则安全:)

答案 1 :(得分:1)

我认为该声明"用户输入“坏”字样输入也不例外:它是预期的。"值得商榷......

但是如果你不想抛出异常,为什么不创建一个isValid()或getValidationErrors()方法呢?

如果有人试图将无效实体保存到数据库,则可以抛出异常。

答案 2 :(得分:0)

您的域名要求在创建人员时,您将提供名字和姓氏。我通常采用的方法是验证输入模型,输入模型可能看起来像;

class PersonInput
{
  var $firstName;
  var $surname;

  public function isValid() { 
    return isset($this->firstName) && isset($this->surname);
  }
}

这实际上是一个警卫,您可以将这些规则放在客户端代码中以尝试阻止此方案,或者您可以使用无效的人员消息从您的帖子返回。我不认为这是一个例外,更像是预期和#34;这就是为什么我写保护代码。您现在可以进入您的域名了;

public function createPerson(PersonInput $input) { 
  if( $input->isValid()) {
     $model->createPerson( $input->firstName, $input->surname);

     return 'success';
  } else {
     return 'person must comtain a valid first name and surname';
  }
}

这只是我的意见,以及如何让我的验证逻辑远离域逻辑。

答案 3 :(得分:0)

我认为$person->surname = '';应该引发错误或异常的设计可以简化。

返回错误

您不希望在分配每个值时始终捕获错误,您需要一个简单的一站式解决方案,如$person->Valididate(),它会查看当前值。然后,当您调用->Save()函数时,它可以先自动调用->Validate(),然后返回False。

返回错误详细信息

但是返回False,甚至错误代码往往是不够的:你想要'谁?为什么?'细节。因此,让我们使用类实例来包含细节,我称之为ItemFieldErrors。它传递给Save(),只查看Save()何时返回False。

public function Validate(&$itemFieldErrors = NULL, $aItem = NULL);

尝试完整的ItemFieldErrors实施。一个阵列就足够了,但我发现这个更有条理,多功能和自我记录。您可以随时随地以任何方式更智能地传递和解析错误详细信息,但通常(如果不总是......)只输出asText()摘要即可。

/**
 * Allows a model to log absent/invalid fields for display to user.
 * Can output string like "Birthdate is invalid, Surname is missing"
 * 
 * Pass this to your Validate() model function.
 */
class ItemFieldErrors
{
  const FIELDERROR_MISSING = 1;
  const FIELDERROR_INVALID = 2;

  protected $itemFieldErrors = array();

  function __construct()
  {
    $this->Clear();
  }

  public function AddErrorMissing($fieldName)
  {
    $this->itemFieldErrors[] = array($fieldName, ItemFieldErrors::FIELDERROR_MISSING);
  }

  public function AddErrorInvalid($fieldName)
  {
    $this->itemFieldErrors[] = array($fieldName, ItemFieldErrors::FIELDERROR_INVALID);
  }

  public function ErrorCount()
  {
    $count = 0;
    foreach ($this->itemFieldErrors as $error) {
      $count++;
    }
    unset($error);
    return $count;
  }

  public function Clear()
  {
    $this->itemFieldErrors = array();
  }

  /**
   * Generate a human readable string to display to user.
   * @return string
   */
  public function AsText()
  {
    $s = '';
    $comma = '';
    foreach($this->itemFieldErrors as $error) {
      switch ($error[1]) {
        case ItemFieldErrors::FIELDERROR_MISSING:
          $s .= $comma . sprintf(qtt("'%s' is absent"), $error[0]);
          break;
        case ItemFieldErrors::FIELDERROR_INVALID:
          $s .= $comma . sprintf(qtt("'%s' is invalid"), $error[0]);
          break;
        default:
          $s .= $comma . sprintf(qtt("'%s' has unforseen issue"), $error[0]);
          break;
      }
      $comma = ', ';
    }
    unset($error);
    return $s;
  }
}

然后当然有$person->Save()需要接收它,所以它可以传递给Validate()。在我的代码中,每当我“加载”来自用户的数据(表单提交)时,就会调用相同的Validate(),而不仅仅是在保存时。

该模型会这样做:

class PersonModel extends BaseModel {

  public $item = array();

  public function Validate(&$itemFieldErrors = NULL, $aItem = NULL) {
    // Prerequisites
    if ($itemFieldErrors === NULL) { $itemFieldErrors = new ItemFieldErrors(); }
    if ($aItem === NULL) { $aItem = $this->item; }

    // Validate
    if (trim($aItem['name'])=='')          { $itemFieldErrors->AddErrorMissing('name'); }
    if (trim($aItem['surname'])=='')       { $itemFieldErrors->AddErrorMissing('surname'); }
    if (!isValidDate($aItem['birthdate'])) { $itemFieldErrors->AddErrorInvalid('birthdate'); }

    return ($itemFieldErrors->ErrorCount() == 0);
  }

  public function Load()..
  public function Save()..
}

这个简单的模型会保存$item中的所有数据,因此它只会将字段公开为$person->item['surname']