Skip to content

Latest commit

 

History

History
1866 lines (1538 loc) · 69.2 KB

File metadata and controls

1866 lines (1538 loc) · 69.2 KB

六、搭建可扩展网站

在本章中,我们将介绍以下主题:

  • 创建通用表单元素生成器
  • 创建 HTML 无线电元素生成器
  • 创建 HTML 选择元素生成器
  • 实现表单工厂
  • 链式$_POST过滤器
  • 链接$_POST验证器
  • 将验证绑定到表单

导言

在本章中,我们将向您展示如何构建生成 HTML 表单元素的类。通用元素生成器可用于文本、文本区域、密码和类似的 HTML 输入类型。之后,我们将展示允许您使用值数组预配置元素的变体。表单工厂配方将所有这些生成器组合在一起,允许您使用单个配置数组呈现整个表单。最后,我们介绍了允许过滤和验证传入$_POST数据的方法。

创建通用表单元素生成器

创建一个只输出表单输入标记(如<input type="text" name="whatever" >)的函数非常容易。然而,为了使表单生成器在一般情况下有用,我们需要考虑更大的前景。除了基本输入标记之外,还有一些其他注意事项:

  • 表单input标记及其关联的 HTML 属性
  • 告诉用户正在输入什么信息的标签
  • 验证后显示输入错误的功能(稍后将详细介绍!)
  • 某种包装器,例如<div>标记或 HTML 表<td>标记

怎么做。。。

  1. 首先,我们定义一个Application\Form\Generic类。这也将在以后用作专用表单元素的基类:

    namespace Application\Form;
    
    class Generic
    {
      // some code ...
    }
  2. 接下来,我们定义一些类常量,这些常量在表单元素生成中通常很有用。

  3. 前三个将成为与单个表单元素的主要组件关联的键。然后定义支持的输入类型和默认值:

    const ROW = 'row';
    const FORM = 'form';
    const INPUT = 'input';
    const LABEL = 'label';
    const ERRORS = 'errors';
    const TYPE_FORM = 'form';
    const TYPE_TEXT = 'text';
    const TYPE_EMAIL = 'email';
    const TYPE_RADIO = 'radio';
    const TYPE_SUBMIT = 'submit';
    const TYPE_SELECT = 'select';
    const TYPE_PASSWORD = 'password';
    const TYPE_CHECKBOX = 'checkbox';
    const DEFAULT_TYPE = self::TYPE_TEXT;
    const DEFAULT_WRAPPER = 'div';
  4. 接下来,我们可以定义属性和设置属性的构造函数。

  5. In this example, we require two properties, $name and $type, as we cannot effectively use the element without these attributes. The other constructor arguments are optional. Furthermore, in order to base one form element on another, we include a provision whereby the second argument, $type, can alternatively be an instance of Application\Form\Generic, in which case we simply run the getters (discussed later) to populate properties:

    protected $name;
    protected $type    = self::DEFAULT_TYPE;
    protected $label   = '';
    protected $errors  = array();
    protected $wrappers;
    protected $attributes;    // HTML form attributes
    protected $pattern =  '<input type="%s" name="%s" %s>';
    
    public function __construct($name, 
                    $type, 
                    $label = '',
                    array $wrappers = array(), 
                    array $attributes = array(),
                    array $errors = array())
    {
      $this->name = $name;
      if ($type instanceof Generic) {
          $this->type       = $type->getType();
          $this->label      = $type->getLabelValue();
          $this->errors     = $type->getErrorsArray();
          $this->wrappers   = $type->getWrappers();
          $this->attributes = $type->getAttributes();
      } else {
          $this->type       = $type ?? self::DEFAULT_TYPE;
          $this->label      = $label;
          $this->errors     = $errors;
          $this->attributes = $attributes;
          if ($wrappers) {
              $this->wrappers = $wrappers;
          } else {
              $this->wrappers[self::INPUT]['type'] =
                self::DEFAULT_WRAPPER;
              $this->wrappers[self::LABEL]['type'] = 
                self::DEFAULT_WRAPPER;
              $this->wrappers[self::ERRORS]['type'] = 
                self::DEFAULT_WRAPPER;
        }
      }
      $this->attributes['id'] = $name;
    }

    注意,$wrappers有三个主子键:INPUTLABELERRORS。这允许我们为标签、输入标记和错误定义单独的包装器。

  6. 在定义为标签、输入标记和错误生成 HTML 的核心方法之前,我们应该定义一个getWrapperPattern()方法,该方法将为标签、输入和错误显示生成合适的包装标记。

  7. 例如,如果包装器被定义为<div>,并且其属性包括['class' => 'label'],则此方法将返回一个如下所示的sprintf()格式模式:<div class="label">%s</div>。例如,为标签生成的最终 HTML 将替换%s

  8. 以下是getWrapperPattern()方法的外观:

    public function getWrapperPattern($type)
    {
      $pattern = '<' . $this->wrappers[$type]['type'];
      foreach ($this->wrappers[$type] as $key => $value) {
        if ($key != 'type') {
          $pattern .= ' ' . $key . '="' . $value . '"';
        }
      }
      $pattern .= '>%s</' . $this->wrappers[$type]['type'] . '>';
      return $pattern;
    }
  9. 我们现在准备定义getLabel()方法。此方法只需使用sprintf()

    public function getLabel()
    {
      return sprintf($this->getWrapperPattern(self::LABEL), 
                     $this->label);
    }

    将标签插入包装器即可

  10. 为了生成核心input标记,我们需要一种方法来组装属性。幸运的是,只要它们以关联数组的形式提供给构造函数,就很容易实现。在这种情况下,我们需要做的就是定义一个getAttribs()方法,该方法生成一个由空格分隔的键值对字符串。我们使用trim()返回最终值以删除多余的空格。

  11. 如果元素包含valuehref属性,出于安全原因,我们应该在假设这些值是或可能是用户提供的(因此值得怀疑)的基础上对其进行转义。因此,我们需要添加一个if语句来检查并使用htmlspecialchars()urlencode()

```php
public function getAttribs()
{
  foreach ($this->attributes as $key => $value) {
    $key = strtolower($key);
    if ($value) {
      if ($key == 'value') {
        if (is_array($value)) {
            foreach ($value as $k => $i) 
              $value[$k] = htmlspecialchars($i);
        } else {
            $value = htmlspecialchars($value);
        }
      } elseif ($key == 'href') {
          $value = urlencode($value);
      }
      $attribs .= $key . '="' . $value . '" ';
    } else {
        $attribs .= $key . ' ';
    }
  }
  return trim($attribs);
}
```
  1. 对于核心输入标记,我们将逻辑分为两个单独的方法。主要方法getInputOnly()只生成*HTML 输入标记。第二种方法getInputWithWrapper()生成嵌入到包装器中的输入。拆分的原因是在创建派生类时,例如生成单选按钮的类,我们不需要包装器:
```php
public function getInputOnly()
{
  return sprintf($this->pattern, $this->type, $this->name, 
                 $this->getAttribs());
}

public function getInputWithWrapper()
{
  return sprintf($this->getWrapperPattern(self::INPUT), 
                 $this->getInputOnly());
}
```* 
  1. 现在我们定义一个显示元素验证错误的方法。我们假设错误将以数组的形式提供。如果没有错误,我们将返回一个空字符串。否则,错误被呈现为<ul><li>error 1</li><li>error 2</li></ul>,以此类推:
```php
public function getErrors()
{
  if (!$this->errors || count($this->errors == 0)) return '';
  $html = '';
  $pattern = '<li>%s</li>';
  $html .= '<ul>';
  foreach ($this->errors as $error)
  $html .= sprintf($pattern, $error);
  $html .= '</ul>';
  return sprintf($this->getWrapperPattern(self::ERRORS), $html);
}
```
  1. 对于某些属性,我们可能需要对属性的各个方面进行更有限的控制。例如,我们可能需要在已经存在的错误数组中添加一个错误。此外,设置单个属性可能也很有用:
```php
public function setSingleAttribute($key, $value)
{
  $this->attributes[$key] = $value;
}
public function addSingleError($error)
{
  $this->errors[] = $error;
}
```
  1. 最后,我们定义了允许我们检索或设置属性值的 getter 和 setter。例如,您可能已经注意到,$pattern的默认值是<input type="%s" name="%s" %s>。对于某些标记(例如,selectform标记),我们需要将此属性设置为不同的值:
```php
public function setPattern($pattern)
{
  $this->pattern = $pattern;
}
public function setType($type)
{
  $this->type = $type;
}
public function getType()
{
  return $this->type;
}
public function addSingleError($error)
{
  $this->errors[] = $error;
}
// define similar get and set methods
// for name, label, wrappers, errors and attributes
```
  1. 我们还需要添加将给出标签值(而不是 HTML)的方法,以及错误数组:
```php
public function getLabelValue()
{
  return $this->label;
}
public function getErrorsArray()
{
  return $this->errors;
}
```

它是如何工作的。。。

确保将前面的所有代码复制到一个Application\Form\Generic类中。然后,您可以定义一个chap_06_form_element_generator.php调用脚本,用于设置自动加载并锚定新类:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Form\Generic;

接下来,定义包装器。为了进行说明,我们将使用 HTML 表数据和标题标记。请注意,标签使用TH,而输入和错误使用TD

$wrappers = [
  Generic::INPUT => ['type' => 'td', 'class' => 'content'],
  Generic::LABEL => ['type' => 'th', 'class' => 'label'],
  Generic::ERRORS => ['type' => 'td', 'class' => 'error']
];

现在可以通过向构造函数传递参数来定义电子邮件元素:

$email = new Generic('email', Generic::TYPE_EMAIL, 'Email', $wrappers,
                    ['id' => 'email',
                     'maxLength' => 128,
                     'title' => 'Enter address',
                     'required' => '']);

或者,使用 setter 定义密码元素:

$password = new Generic('password', $email);
$password->setType(Generic::TYPE_PASSWORD);
$password->setLabel('Password');
$password->setAttributes(['id' => 'password',
                          'title' => 'Enter your password',
                          'required' => '']);

最后,请确保定义提交按钮:

$submit = new Generic('submit', 
  Generic::TYPE_SUBMIT,
  'Login',
  $wrappers,
  ['id' => 'submit','title' => 'Click to login','value' => 'Click Here']);

实际的显示逻辑可能如下所示:

<div class="container">
  <!-- Login Form -->
  <h1>Login</h1>
  <form name="login" method="post">
  <table id="login" class="display" 
    cellspacing="0" width="100%">
    <tr><?= $email->render(); ?></tr>
    <tr><?= $password->render(); ?></tr>
    <tr><?= $submit->render(); ?></tr>
    <tr>
      <td colspan=2>
        <br>
        <?php var_dump($_POST); ?>
      </td>
    </tr>
  </table>
  </form>
</div>

以下是实际输出:

How it works...

创建 HTML 无线电元素生成器

单选按钮元素生成器与通用 HTML 表单元素生成器具有相似之处。与任何通用元素一样,一组单选按钮需要能够显示整体标签和错误。然而,有两个主要区别:

  • 通常,您需要两个或多个单选按钮
  • 每个按钮都需要有自己的标签

怎么做。。。

  1. 首先,创建一个新的Application\Form\Element\Radio类,扩展Application\Form\Generic

    namespace Application\Form\Element;
    use Application\Form\Generic;
    class Radio extends Generic
    {
     // code
    }
  2. 接下来,我们定义与一组单选按钮的特殊需求相关的类常量和属性。

  3. 在本图中,我们需要一个spacer,它将放置在单选按钮及其标签之间。我们还需要决定单选按钮标签是放在之前还是放在实际按钮之后,因此,我们使用$after标志。如果我们需要一个默认值,或者如果我们正在显示现有的表单数据,我们需要一种指定所选键的方法。最后,我们需要一个选项数组,从中填充按钮列表:

    const DEFAULT_AFTER = TRUE;
    const DEFAULT_SPACER = '&nbps;';
    const DEFAULT_OPTION_KEY = 0;
    const DEFAULT_OPTION_VALUE = 'Choose';
    
    protected $after = self::DEFAULT_AFTER;
    protected $spacer = self::DEFAULT_SPACER;
    protected $options = array();
    protected $selectedKey = DEFAULT_OPTION_KEY;
  4. 考虑到我们正在扩展Application\Form\Generic,我们可以选择扩展__construct()方法,或者简单地定义一个可用于设置特定选项的方法。对于这个例子,我们选择了后一种方法。

  5. 为确保填充属性$this->options,将第一个参数($options定义为必填(无默认值)。所有其他参数都是可选的。

    public function setOptions(array $options, 
      $selectedKey = self::DEFAULT_OPTION_KEY, 
      $spacer = self::DEFAULT_SPACER,
      $after  = TRUE)
    {
      $this->after = $after;
      $this->spacer = $spacer;
      $this->options = $options;
      $this->selectedKey = $selectedKey;
    }  
  6. 最后,我们准备重写核心getInputOnly()方法。

  7. 我们将id属性保存到自变量$baseId中,然后将其与$count组合,使每个id属性都是唯一的。如果定义了与所选关键点关联的选项,则将其指定为值;否则,我们使用默认值:

    public function getInputOnly()
    {
      $count  = 1;
      $baseId = $this->attributes['id'];
  8. foreach()循环中,我们检查键是否为所选键。如果是,则为该单选按钮添加checked属性。然后我们调用父类getInputOnly()方法返回每个按钮的 HTML。请注意,输入元素的value属性是 options 数组键。按钮标签为选项数组元素值:

    foreach ($this->options as $key => $value) {
      $this->attributes['id'] = $baseId . $count++;
      $this->attributes['value'] = $key;
      if ($key == $this->selectedKey) {
          $this->attributes['checked'] = '';
      } elseif (isset($this->attributes['checked'])) {
                unset($this->attributes['checked']);
      }
      if ($this->after) {
          $html = parent::getInputOnly() . $value;
      } else {
          $html = $value . parent::getInputOnly();
      }
      $output .= $this->spacer . $html;
      }
      return $output;
    }

它是如何工作的。。。

将上述代码复制到Application/Form/Element文件夹中的新Radio.php文件中。然后,您可以定义一个chap_06_form_element_radio.php调用脚本,用于设置自动加载并锚定新类:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Form\Generic;
use Application\Form\Element\Radio;

接下来,使用前面配方中定义的$wrappers数组定义包装器。

然后您可以定义一个$status数组,并通过向构造函数传递参数来创建一个元素实例:

$statusList = [
  'U' => 'Unconfirmed',
  'P' => 'Pending',
  'T' => 'Temporary Approval',
  'A' => 'Approved'
];

$status = new Radio('status', 
          Generic::TYPE_RADIO, 
          'Status',
          $wrappers,
          ['id' => 'status']);

现在您可以查看$_GET是否有任何状态输入,并设置选项。任何输入都将成为选定的键。否则,所选关键点为默认值:

$checked = $_GET['status'] ?? 'U';
$status->setOptions($statusList, $checked, '<br>', TRUE);          

最后,别忘了定义一个提交按钮:

$submit = new Generic('submit', 
          Generic::TYPE_SUBMIT,
          'Process',
          $wrappers,
          ['id' => 'submit','title' => 
          'Click to process','value' => 'Click Here']);

显示逻辑可能如下所示:

<form name="status" method="get">
<table id="status" class="display" cellspacing="0" width="100%">
  <tr><?= $status->render(); ?></tr>
  <tr><?= $submit->render(); ?></tr>
  <tr>
    <td colspan=2>
      <br>
      <pre><?php var_dump($_GET); ?></pre>
    </td>
  </tr>
</table>
</form>

以下是实际输出:

How it works...

还有更多。。。

复选框元素生成器与 HTML 单选按钮生成器几乎相同。主要区别在于一组复选框可以选中多个值。因此,您将使用 PHP 数组表示法作为元素名称。元件类型应为Generic::TYPE_CHECKBOX

创建 HTML 选择元素生成器

生成 HTML 单个 select 元素与生成单选按钮的过程类似。然而,标记的结构不同,因为需要生成一个SELECT标记和一系列OPTION标记。

怎么做。。。

  1. 首先,创建一个扩展了Application\Form\Generic的新Application\Form\Element\Select类。

  2. 我们之所以扩展Generic而不是Radio是因为元素的结构完全不同:

    namespace Application\Form\Element;
    
    use Application\Form\Generic;
    
    class Select extends Generic
    {
      // code
    }
  3. 类常量和属性只需稍微添加到Application\Form\Generic中即可。与单选按钮或复选框不同,无需说明间隔符或所选文本的位置:

    const DEFAULT_OPTION_KEY = 0;
    const DEFAULT_OPTION_VALUE = 'Choose';
    
    protected $options;
    protected $selectedKey = DEFAULT_OPTION_KEY;
  4. 现在我们将注意力转向设置选项。由于 HTML select 元素可以选择单个或多个值,$selectedKey属性可以是字符串或数组。因此,我们不为该属性添加类型提示。然而,重要的是,我们要确定multiple属性是否已设置。这可以通过继承父类从$this->attributes属性获得。

  5. If the multiple attribute has been set, it's important to formulate the name attribute as an array. Accordingly, we would append [] to the name if this were the case:

    public function setOptions(array $options, $selectedKey = self::DEFAULT_OPTION_KEY)
    {
      $this->options = $options;
      $this->selectedKey = $selectedKey;
      if (isset($this->attributes['multiple'])) {
        $this->name .= '[]';
      } 
    }

    在 PHP 中,如果设置了 HTML selectmultiple属性,并且name属性未指定为数组,则只返回一个值!

  6. 在定义核心getInputOnly()方法之前,我们需要定义一个方法来生成select标记。然后我们使用sprintf()、使用$pattern$namegetAttribs()的返回值作为参数返回最终的 HTML。

  7. 我们将$pattern的默认值替换为<select name="%s" %s>。然后,我们循环遍历属性,将它们作为键值对添加,中间有空格:

    protected function getSelect()
    {
      $this->pattern = '<select name="%s" %s> ' . PHP_EOL;
      return sprintf($this->pattern, $this->name, 
      $this->getAttribs());
    }
  8. 接下来,我们定义一个方法来获取将与select标记关联的option标记。

  9. 正如您所记得的,$this->options数组中的表示返回值,而数组中的部分表示将出现在屏幕上的文本。如果$this->selectedKey是数组形式,我们检查该值是否在数组中。否则,我们假设$this-> selectedKey是一个字符串,我们只需确定它是否等于键。如果选择的键匹配,则添加selected属性:

    protected function getOptions()
    {
      $output = '';
      foreach ($this->options as $key => $value) {
        if (is_array($this->selectedKey)) {
            $selected = (in_array($key, $this->selectedKey)) 
            ? ' selected' : '';
        } else {
            $selected = ($key == $this->selectedKey) 
            ? ' selected' : '';
        }
            $output .= '<option value="' . $key . '"' 
            . $selected  . '>' 
            . $value 
            . '</option>';
      }
      return $output;
    }
  10. 最后,我们准备重写核心getInputOnly()方法。

  11. 您将注意到,此方法的逻辑只需要捕获前面代码中描述的getSelect()getOptions()方法的返回值。我们还需要添加结束</select>标签:

```php
public function getInputOnly()
{
  $output = $this->getSelect();
  $output .= $this->getOptions();
  $output .= '</' . $this->getType() . '>'; 
  return $output;
}
```

它是如何工作的。。。

将上述代码复制到Application/Form/Element文件夹中的新Select.php文件中。然后定义一个chap_06_form_element_select.php调用脚本,用于设置自动加载并锚定新类:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Form\Generic;
use Application\Form\Element\Select;

接下来,使用第一个配方中定义的数组$wrappers定义包装器。您还可以使用创建 HTML 无线电元素生成器配方中定义的$statusList数组。然后可以创建SELECT元素的实例。第一个实例为单选,第二个实例为多个:

$status1 = new Select('status1', 
           Generic::TYPE_SELECT, 
           'Status 1',
           $wrappers,
           ['id' => 'status1']);
$status2 = new Select('status2', 
           Generic::TYPE_SELECT, 
           'Status 2',
           $wrappers,
           ['id' => 'status2', 
            'multiple' => '', 
            'size' => '4']);

查看$_GET是否有状态输入,并设置选项。任何输入都将成为选定的键。否则,所选关键点为默认值。您还记得,第二个实例是 multiple select,因此从$_GET获得的值和默认设置都应该是数组形式:

$checked1 = $_GET['status1'] ?? 'U';
$checked2 = $_GET['status2'] ?? ['U'];
$status1->setOptions($statusList, $checked1);
$status2->setOptions($statusList, $checked2);

最后,一定要定义一个提交按钮(如本章创建通用表单元素生成器配方中所示)。

实际的显示逻辑与单选按钮配方相同,只是我们需要呈现两个单独的 HTML select 实例:

<form name="status" method="get">
<table id="status" class="display" cellspacing="0" width="100%">
  <tr><?= $status1->render(); ?></tr>
  <tr><?= $status2->render(); ?></tr>
  <tr><?= $submit->render(); ?></tr>
  <tr>
    <td colspan=2>
      <br>
      <pre>
        <?php var_dump($_GET); ?>
      </pre>
    </td>
  </tr>
</table>
</form>

以下是实际输出:

How it works...

此外,您还可以在查看源页面中看到元素是如何显示的:

How it works...

实施表单工厂

表单工厂的目的是从单个配置数组生成可用的表单对象。表单对象应该能够检索它包含的各个元素,以便生成输出。

怎么做。。。

  1. 首先,让我们创建一个名为Application\Form\Factory的类来包含工厂代码。它将只有一个属性$elements和一个 getter:

    namespace Application\Form;
    
    class Factory
    {
      protected $elements;
      public function getElements()
      {
        return $this->elements;
      }
      // remaining code
    }
  2. 在定义主窗体生成方法之前,重要的是考虑我们打算接收什么配置格式,以及生成表单的确切内容。在本例中,我们假设生成将生成一个具有$elements属性的Factory实例。此属性将是Application\Form\GenericApplication\Form\Element类的数组。

  3. 我们现在准备处理generate()方法。这将在配置数组中循环,创建相应的Application\Form\GenericApplication\Form\Element\*对象,然后这些对象将存储在$elements数组中。新方法将接受配置数组作为参数。将此方法定义为静态方法很方便,这样我们就可以使用不同的配置块生成所需的任意多个实例。

  4. 我们创建了一个Application\Form\Factory实例,然后开始在配置数组中循环:

    public static function generate(array $config)
    {
      $form = new self();
      foreach ($config as $key => $p) {
  5. 接下来,我们检查Application\Form\Generic类的构造函数中可选的参数:

      $p['errors'] = $p['errors'] ?? array();
      $p['wrappers'] = $p['wrappers'] ?? array();
      $p['attributes'] = $p['attributes'] ?? array();
  6. 现在,所有构造函数参数都已就位,我们可以创建表单元素实例,然后将其存储在$elements

      $form->elements[$key] = new $p['class']
      (
        $key, 
        $p['type'],
        $p['label'],
        $p['wrappers'],
        $p['attributes'],
        $p['errors']
      );

  7. 接下来,我们将注意力转向选项。如果设置了options参数,我们使用list()将数组值提取到变量中。然后,我们使用switch()测试元素类型,并使用适当数量的参数

        if (isset($p['options'])) {
          list($a,$b,$c,$d) = $p['options'];
          switch ($p['type']) {
            case Generic::TYPE_RADIO    :
            case Generic::TYPE_CHECKBOX :
              $form->elements[$key]->setOptions($a,$b,$c,$d);
              break;
            case Generic::TYPE_SELECT   :
              $form->elements[$key]->setOptions($a,$b);
              break;
            default                     :
              $form->elements[$key]->setOptions($a,$b);
              break;
          }
        }
      }

    运行setOptions()

  8. 最后,我们返回表单对象并结束方法:

      return $form;
    } 
  9. 理论上,在这一点上,我们可以通过简单地迭代元素数组并运行render()方法,轻松地在视图逻辑中呈现表单。视图逻辑可能如下所示:

    <form name="status" method="get">
      <table id="status" class="display" cellspacing="0" width="100%">
        <?php foreach ($form->getElements() as $element) : ?>
          <?php echo $element->render(); ?>
        <?php endforeach; ?>
      </table>
    </form>
  10. 最后,我们返回表单对象并结束该方法。

  11. 接下来,我们需要在Application\Form\Element

```php
namespace Application\Form\Element;
class Form extends Generic
{
  public function getInputOnly()
  {
    $this->pattern = '<form name="%s" %s> ' . PHP_EOL;
    return sprintf($this->pattern, $this->name, 
                   $this->getAttribs());
  }
  public function closeTag()
  {
    return '</' . $this->type . '>';
  }
}
```

下定义一个离散的`Form`类
  1. 回到Application\Form\Factory类,我们现在需要定义一个简单的方法,该方法返回一个sprintf()包装器模式,作为输入的信封。作为一个示例,如果包装器是具有属性class="test"div,我们将生成以下模式:<div class="test">%s</div>。然后,我们的内容将被sprintf()函数取代%s
```php
protected function getWrapperPattern($wrapper)
{
  $type = $wrapper['type'];
  unset($wrapper['type']);
  $pattern = '<' . $type;
  foreach ($wrapper as $key => $value) {
    $pattern .= ' ' . $key . '="' . $value . '"';
  }
  $pattern .= '>%s</' . $type . '>';
  return $pattern;
}
```
  1. 最后,我们准备定义一个进行整体表单呈现的方法。我们为每个表单行获取包装器sprintf()模式。然后,我们循环遍历元素,渲染每个元素,并以行模式包装输出。接下来,我们生成一个Application\Form\Element\Form实例。然后我们检索表单包装器sprintf()模式并检查form_tag_inside_wrapper标志,该标志告诉我们是否需要将表单标签放置在表单包装器的内部或外部:
```php
public static function render($form, $formConfig)
{
  $rowPattern = $form->getWrapperPattern(
  $formConfig['row_wrapper']);
  $contents   = '';
  foreach ($form->getElements() as $element) {
    $contents .= sprintf($rowPattern, $element->render());
  }
  $formTag = new Form($formConfig['name'], 
                  Generic::TYPE_FORM, 
                  '', 
                  array(), 
                  $formConfig['attributes']); 

  $formPattern = $form->getWrapperPattern(
  $formConfig['form_wrapper']);
  if (isset($formConfig['form_tag_inside_wrapper']) 
      && !$formConfig['form_tag_inside_wrapper']) {
        $formPattern = '%s' . $formPattern . '%s';
        return sprintf($formPattern, $formTag->getInputOnly(), 
        $contents, $formTag->closeTag());
  } else {
        return sprintf($formPattern, $formTag->getInputOnly() 
        . $contents . $formTag->closeTag());
  }
}
```

它是如何工作的。。。

参考前面的代码,创建Application\Form\FactoryApplication\Form\Element\Form类。

接下来,您可以定义一个chap_06_form_factor.php调用脚本,用于设置自动加载并锚定新类:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Form\Generic;
use Application\Form\Factory;

接下来,使用第一个配方中定义的$wrappers数组定义包装器。您还可以使用第二个配方中定义的$statusList数组。

查看$_POST是否有状态输入。任何输入都将成为选定的键。否则,所选关键点为默认值。

$email    = $_POST['email']   ?? '';
$checked0 = $_POST['status0'] ?? 'U';
$checked1 = $_POST['status1'] ?? 'U';
$checked2 = $_POST['status2'] ?? ['U'];
$checked3 = $_POST['status3'] ?? ['U'];

现在您可以定义整个表单配置。nameattributes参数用于配置form标签本身。其他两个参数表示表单级和行级包装器。最后,我们提供了一个form_tag_inside_wrapper标志,指示表单标签不应出现在包装器中(即<table>。如果包装器是<div>,我们会将此标志设置为TRUE

$formConfig = [ 
  'name'         => 'status_form',
  'attributes'   => ['id'=>'statusForm','method'=>'post', 'action'=>'chap_06_form_factory.php'],
  'row_wrapper'  => ['type' => 'tr', 'class' => 'row'],
  'form_wrapper' => ['type'=>'table','class'=>'table', 'id'=>'statusTable',
                     'class'=>'display','cellspacing'=>'0'],
                     'form_tag_inside_wrapper' => FALSE,
];

接下来,定义一个数组,保存工厂创建的每个表单元素的参数。数组键成为表单元素的名称,并且必须是唯一的:

$config = [
  'email' => [  
    'class'     => 'Application\Form\Generic',
    'type'      => Generic::TYPE_EMAIL, 
    'label'     => 'Email', 
    'wrappers'  => $wrappers,
    'attributes'=> ['id'=>'email','maxLength'=>128, 'title'=>'Enter address',
                    'required'=>'','value'=>strip_tags($email)]
  ],
  'password' => [
    'class'      => 'Application\Form\Generic',
    'type'       => Generic::TYPE_PASSWORD,
    'label'      => 'Password',
    'wrappers'   => $wrappers,
    'attributes' => ['id'=>'password',
    'title'      => 'Enter your password',
    'required'   => '']
  ],
  // etc.
];

最后,确保生成表单:

$form = Factory::generate($config);

实际的显示逻辑非常简单,我们简单地称之为表单级render()方法:

<?= $form->render($form, $formConfig); ?>

以下是实际输出:

How it works...

链接$\u 后过滤器

在处理用户从在线表单提交的数据时,适当的过滤和验证是一个常见问题。它也可以说是网站的头号安全漏洞。此外,将过滤器和验证器分散在整个应用程序中可能会非常尴尬。链接机制可以巧妙地解决这些问题,还可以让您控制过滤器和验证器的处理顺序。

怎么做。。。

  1. There is a little-known PHP function, filter_input_array(), that, at first glance, seems well suited for this task. Looking more deeply into its functionality, however, it soon becomes apparent that this function was designed in the early days, and is not up to modern requirements for protection against attack and flexibility. Accordingly, we will instead present a much more flexible mechanism based on an array of callbacks performing filtering and validation.

    过滤验证之间的区别在于过滤可能会移除或转换值。另一方面,验证使用适合数据性质的标准测试数据,并返回布尔结果。

  2. 为了增加的灵活性,我们将使我们的基本过滤器和验证类相对较轻。我们的意思是没有定义任何特定的过滤器或验证方法。相反,我们将完全基于回调的配置数组进行操作。为了确保过滤和验证结果的兼容性,我们还将定义一个特定的结果对象Application\Filter\Result

  3. Result类的主要功能是保存一个$item值,它将是过滤后的值或验证的布尔结果。另一个属性$messages将保存在筛选或验证操作期间填充的消息数组。在构造函数中,为$messages 提供的值被表示为一个数组。您可能会注意到这两个属性都定义为public。这是为了方便访问:

    namespace Application\Filter;
    
    class Result
    {
    
      public $item;  // (mixed) filtered data | (bool) result of validation
      public $messages = array();  // [(string) message, (string) message ]
    
      public function __construct($item, $messages)
      {
        $this->item = $item;
        if (is_array($messages)) {
            $this->messages = $messages;
        } else {
            $this->messages = [$messages];
        }
      }
  4. 我们还定义了一个方法,允许我们将这个Result实例与另一个Result实例合并。这很重要,因为在某个时刻,我们将通过一系列过滤器处理相同的值。在这种情况下,我们希望新筛选的值覆盖现有值,但希望合并消息:

    public function mergeResults(Result $result)
    {
      $this->item = $result->item;
      $this->mergeMessages($result);
    }
    
    public function mergeMessages(Result $result)
    {
      if (isset($result->messages) && is_array($result->messages)) {
        $this->messages = array_merge($this->messages, $result->messages);
      }
    }
  5. 最后,为了完成这个类的方法,我们添加了一个合并验证结果的方法。这里需要考虑的重要因素是,FALSE任何值,在验证链上下,都必须导致整个结果为FALSE

    public function mergeValidationResults(Result $result)
    {
      if ($this->item === TRUE) {
        $this->item = (bool) $result->item;
      }
      $this->mergeMessages($result);
      }
    
    }
  6. 接下来,为了确保回调产生兼容的结果,我们将定义一个Application\Filter\CallbackInterface接口。您会注意到,我们正在利用 PHP7 数据类型返回值的能力,以确保我们得到一个Result实例作为返回:

    namespace Application\Filter;
    interface CallbackInterface
    {
      public function __invoke ($item, $params) : Result;
    }
  7. 每个回调都应该引用同一组消息。因此,我们定义了一个具有一系列静态属性的Application\Filter\Messages类。我们提供了设置所有消息的方法,或者只设置一条消息。$messages属性已制作public以便于访问:

    namespace Application\Filter;
    class Messages
    {
      const MESSAGE_UNKNOWN = 'Unknown';
      public static $messages;
      public static function setMessages(array $messages)
      {
        self::$messages = $messages;
      }
      public static function setMessage($key, $message)
      {
        self::$messages[$key] = $message;
      }
      public static function getMessage($key)
      {
        return self::$messages[$key] ?? self::MESSAGE_UNKNOWN;
      }
    }
  8. 我们现在可以定义一个实现核心功能的Application\Web\AbstractFilter类。如前所述,此类将相对轻量级,我们不需要担心特定的过滤器或验证器,因为它们将通过配置提供。我们使用作为 PHP7标准 PHP 库SPL的一部分提供的UnexpectedValueException类以便在其中一个回调未实现CallbackInterface

    namespace Application\Filter;
    use UnexpectedValueException;
    abstract class AbstractFilter
    {
      // code described in the next several bullets

    时抛出描述性异常

  9. 首先,我们定义有用的类常量,这些类常量包含各种内务处理值。这里显示的最后四个控制要显示的消息的格式,以及如何描述缺少的数据:

    const BAD_CALLBACK = 'Must implement CallbackInterface';
    const DEFAULT_SEPARATOR = '<br>' . PHP_EOL;
    const MISSING_MESSAGE_KEY = 'item.missing';
    const DEFAULT_MESSAGE_FORMAT = '%20s : %60s';
    const DEFAULT_MISSING_MESSAGE = 'Item Missing';
  10. 接下来,我们定义核心属性。$separator与过滤和验证消息一起使用。$callbacks表示执行筛选和验证的回调数组。$assignments将数据字段映射到过滤器和/或验证器。$missingMessage表示为一个属性,以便可以覆盖它(即,对于多语言网站)。最后,$resultsApplication\Filter\Result对象的数组,由过滤或验证操作填充:

```php
protected $separator;    // used for message display
protected $callbacks;
protected $assignments;
protected $missingMessage;
protected $results = array();
```
  1. 此时,我们可以构建__construct()方法。它的主要功能是设置回调和赋值的数组。它还可以为分隔符(用于消息显示)和缺少的消息
```php
public function __construct(array $callbacks, array $assignments, 
                            $separator = NULL, $message = NULL)
{
  $this->setCallbacks($callbacks);
  $this->setAssignments($assignments);
  $this->setSeparator($separator ?? self::DEFAULT_SEPARATOR);
  $this->setMissingMessage($message 
                           ?? self::DEFAULT_MISSING_MESSAGE);
}
```

设置值或接受默认值
  1. 接下来,我们定义了一系列允许我们设置或删除回调的方法。请注意,我们允许获取和设置单个回调。如果您有一组通用回调,并且只需要修改一个回调,那么这将非常有用。您还将注意到,setOneCall()检查回调是否实现了CallbackInterface。如果没有,则抛出一个UnexpectedValueException
```php
public function getCallbacks()
{
  return $this->callbacks;
}

public function getOneCallback($key)
{
  return $this->callbacks[$key] ?? NULL;
}

public function setCallbacks(array $callbacks)
{
  foreach ($callbacks as $key => $item) {
    $this->setOneCallback($key, $item);
  }
}

public function setOneCallback($key, $item)
{
  if ($item instanceof CallbackInterface) {
      $this->callbacks[$key] = $item;
  } else {
      throw new UnexpectedValueException(self::BAD_CALLBACK);
  }
}

public function removeOneCallback($key)
{
  if (isset($this->callbacks[$key])) 
  unset($this->callbacks[$key]);
}
```
  1. 结果处理方法非常简单。为了方便起见,我们添加了getItemsAsArray(),否则getResults()将返回一个Result对象数组:
```php
public function getResults()
{
  return $this->results;
}

public function getItemsAsArray()
{
  $return = array();
  if ($this->results) {
    foreach ($this->results as $key => $item) 
    $return[$key] = $item->item;
  }
  return $return;
}
```
  1. 检索消息只是在$this ->results数组中循环并提取$messages属性的问题。为了方便起见,我们还添加了带有一些格式选项的getMessageString()。为了方便地生成消息数组,我们使用 PHP7yield from语法。这具有将getMessages()变成委托生成器的效果。消息数组成为子生成器
```php
public function getMessages()
{
  if ($this->results) {
      foreach ($this->results as $key => $item) 
      if ($item->messages) yield from $item->messages;
  } else {
      return array();
  }
}

public function getMessageString($width = 80, $format = NULL)
{
  if (!$format)
  $format = self::DEFAULT_MESSAGE_FORMAT . $this->separator;
  $output = '';
  if ($this->results) {
    foreach ($this->results as $key => $value) {
      if ($value->messages) {
        foreach ($value->messages as $message) {
          $output .= sprintf(
            $format, $key, trim($message));
        }
      }
    }
  }
  return $output;
}
```
  1. 最后,我们定义了一组混合的有用的 getter 和 setter:
```php
  public function setMissingMessage($message)
  {
    $this->missingMessage = $message;
  }
  public function setSeparator($separator)
  {
    $this->separator = $separator;
  }
  public function getSeparator()
  {
    return $this->separator;
  }
  public function getAssignments()
  {
    return $this->assignments;
  }
  public function setAssignments(array $assignments)
  {
    $this->assignments = $assignments;
  }
  // closing bracket for class AbstractFilter
}
```
  1. 过滤和验证虽然经常一起执行,但也经常分开执行。因此,我们为每个类定义离散类。我们从Application\Filter\Filter开始。我们让这个类扩展AbstractFilter以提供前面描述的核心功能:
```php
namespace Application\Filter;
class Filter extends AbstractFilter
{
  // code
}
```
  1. 在这个类中,我们定义了一个核心process()方法,该方法扫描一个数据数组,并根据分配数组应用过滤器。如果没有为该数据集分配过滤器,我们只需返回NULL
```php
public function process(array $data)
{
  if (!(isset($this->assignments) 
      && count($this->assignments))) {
        return NULL;
  }
```
  1. 否则,我们将$this->results初始化为Result对象数组,其中$item属性是$data的原始值,$messages属性是空数组:
```php
foreach ($data as $key => $value) {
  $this->results[$key] = new Result($value, array());
}
```
  1. 然后我们复制一份$this->assignments并检查是否有全局过滤器(由“*键标识)。如果有,我们运行processGlobal(),然后取消设置“*键:
```php
$toDo = $this->assignments;
if (isset($toDo['*'])) {
  $this->processGlobalAssignment($toDo['*'], $data);
  unset($toDo['*']);
}
```
  1. 最后,我们通过调用processAssignment()
```php
foreach ($toDo as $key => $assignment) {
  $this->processAssignment($assignment, $key);
}
```

循环所有剩余的分配
  1. As you will recall, each assignment is keyed to the data field, and represents an array of callbacks for that field. Thus, in processGlobalAssignment() we need to loop through the array of callbacks. In this case, however, because these assignments are global, we also need to loop through the entire data set, and apply each global filter in turn:
```php
protected function processGlobalAssignment($assignment, $data)
{
  foreach ($assignment as $callback) {
    if ($callback === NULL) continue;
    foreach ($data as $k => $value) {
      $result = $this->callbacks[$callback['key']]($this->results[$k]->item,
      $callback['params']);
      $this->results[$k]->mergeResults($result);
    }
  }
}
```

### 注

棘手的是这行代码:

```php
$result = $this->callbacks[$callback['key']]($this ->results[$k]->item, $callback['params']);
```

记住,每个回调实际上都是一个匿名类,它定义了 PHP magic`__invoke()`方法。提供的参数是要筛选的实际数据项和参数数组。通过运行`$this->callbacks[$callback['key']]()`我们实际上是在神奇地调用`__invoke()`。
  1. When we define processAssignment(), in a manner akin to processGlobalAssignment(), we need to execute each remaining callback assigned to each data key:
```php
  protected function processAssignment($assignment, $key)
  {
    foreach ($assignment as $callback) {
      if ($callback === NULL) continue;
      $result = $this->callbacks[$callback['key']]($this->results[$key]->item, 
                                 $callback['params']);
      $this->results[$key]->mergeResults($result);
    }
  }
}  // closing brace for Application\Filter\Filter
```

### 注

重要的是,任何更改原始用户提供数据的过滤操作都应显示一条消息,指示已进行了更改。这可以成为审计跟踪的一部分,以保护您在未经用户知情或同意的情况下进行更改时免受潜在的法律责任。

它是如何工作的。。。

创建一个Application\Filter文件夹。在此文件夹中,使用前面步骤中的代码创建以下类文件:

|

应用程序\过滤器*类文件

|

这些步骤中描述的代码

| | --- | --- | | Result.php | 3 - 5 | | CallbackInterface.php | 6. | | Messages.php | 7. | | AbstractFilter.php | 8 - 15 | | Filter.php | 16 - 22 |

接下来,使用步骤 5 中讨论的代码,在chap_06_post_data_config_messages.php文件中配置消息数组。每个回调都引用Messages::$messages属性。以下是一个示例配置:

<?php
use Application\Filter\Messages;
Messages::setMessages(
  [
    'length_too_short' => 'Length must be at least %d',
    'length_too_long'  => 'Length must be no more than %d',
    'required'         => 'Please be sure to enter a value',
    'alnum'            => 'Only letters and numbers allowed',
    'float'            => 'Only numbers or decimal point',
    'email'            => 'Invalid email address',
    'in_array'         => 'Not found in the list',
    'trim'             => 'Item was trimmed',
    'strip_tags'       => 'Tags were removed from this item',
    'filter_float'     => 'Converted to a decimal number',
    'phone'            => 'Phone number is [+n] nnn-nnn-nnnn',
    'test'             => 'TEST',
    'filter_length'    => 'Reduced to specified length',
  ]
);

接下来,创建一个包含过滤回调的配置的chap_06_post_data_config_callbacks.php回调配置文件,如步骤 4 所述。每个回调都应遵循以下通用模板:

'callback_key' => new class () implements CallbackInterface 
{
  public function __invoke($item, $params) : Result
  {
    $changed  = array();
    $filtered = /* perform filtering operation on $item */
    if ($filtered !== $item) $changed = Messages::$messages['callback_key'];
    return new Result($filtered, $changed);
  }
}

回调本身必须实现接口并返回一个Result实例。我们可以利用 PHP7匿名类功能,让回调返回实现CallbackInterface的匿名类。下面是筛选回调数组的外观:

use Application\Filter\ { Result, Messages, CallbackInterface };
$config = [ 'filters' => [
  'trim' => new class () implements CallbackInterface 
  {
    public function __invoke($item, $params) : Result
    {
      $changed  = array();
      $filtered = trim($item);
      if ($filtered !== $item) 
      $changed = Messages::$messages['trim'];
      return new Result($filtered, $changed);
    }
  },
  'strip_tags' => new class () 
  implements CallbackInterface 
  {
    public function __invoke($item, $params) : Result
    {
      $changed  = array();
      $filtered = strip_tags($item);
      if ($filtered !== $item)     
      $changed = Messages::$messages['strip_tags'];
      return new Result($filtered, $changed);
    }
  },
  // etc.
]
];

出于测试目的,我们将使用 prospects 表作为目标。我们将不提供来自$_POST的数据,而是构建一个数据的数组:

How it works...

您现在可以创建一个chap_06_post_data_filtering.php脚本来设置自动加载,包括消息和回调配置文件:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
include __DIR__ . '/chap_06_post_data_config_messages.php';
include __DIR__ . '/chap_06_post_data_config_callbacks.php';

然后需要定义表示数据字段和筛选器回调之间映射的分配。使用键*定义一个适用于所有数据的全局过滤器:

$assignments = [
  '*'   => [ ['key' => 'trim', 'params' => []], 
          ['key' => 'strip_tags', 'params' => []] ],
  'first_name'  => [ ['key' => 'length', 
   'params' => ['length' => 128]] ],
  'last_name'  => [ ['key' => 'length', 
   'params' => ['length' => 128]] ],
  'city'          => [ ['key' => 'length', 
   'params' => ['length' => 64]] ],
  'budget'     => [ ['key' => 'filter_float', 'params' => []] ],
];

接下来定义良好不良测试数据:

$goodData = [
  'first_name'      => 'Your Full',
  'last_name'       => 'Name',
  'address'         => '123 Main Street',
  'city'            => 'San Francisco',
  'state_province'  => 'California',
  'postal_code'     => '94101',
  'phone'           => '+1 415-555-1212',
  'country'         => 'US',
  'email'           => 'your@email.address.com',
  'budget'          => '123.45',
];
$badData = [
  'first_name'      => 'This+Name<script>bad tag</script>Valid!',
  'last_name'       => 'ThisLastNameIsWayTooLongAbcdefghijklmnopqrstuvwxyz0123456789Abcdefghijklmnopqrstuvwxyz0123456789Abcdefghijklmnopqrstuvwxyz0123456789Abcdefghijklmnopqrstuvwxyz0123456789',
  //'address'       => '',    // missing
  'city'            => '  ThisCityNameIsTooLong012345678901234567890123456789012345678901234567890123456789  ',
  //'state_province'=> '',    // missing
  'postal_code'     => '!"£$%^Non Alpha Chars',
  'phone'           => ' 12345 ',
  'country'         => 'XX',
  'email'           => 'this.is@not@an.email',
  'budget'          => 'XXX',
];

最后,您可以创建一个Application\Filter\Filter实例,并测试数据:

$filter = new Application\Filter\Filter(
$config['filters'], $assignments);
$filter->setSeparator(PHP_EOL);
  $filter->process($goodData);
echo $filter->getMessageString();
  var_dump($filter->getItemsAsArray());

$filter->process($badData);
echo $filter->getMessageString();
var_dump($filter->getItemsAsArray());

处理良好数据时,除了一条指示浮点字段的值已从字符串转换为float的消息外,不会产生其他消息。另一方面,数据产生以下输出:

How it works...

您还将注意到标记已从first_name中删除,并且last_namecity都被截断。

还有更多。。。

filter_input_array()函数采用两个参数:输入源(以预定义的常量的形式,用于指示$_*PHP 超全局变量之一,即$_POST),匹配字段定义数组作为键,过滤器或验证器作为值。此函数不仅执行筛选操作,还执行验证。标记为消毒的标志实际上是过滤器。

另见

filter_input_array()的文档和示例见http://php.net/manual/en/function.filter-input-array.php 。您还可以查看上提供的过滤器的不同类型 http://php.net/manual/en/filter.filters.php

链接$邮政验证器

此配方的起重已经在前面的配方中完成。核心功能由Application\Filter\AbstractFilter定义。实际的验证由一组验证回调执行。

怎么做。。。

  1. 查看前面的配方,链接$_ 后过滤器。我们将使用此配方中的所有类和配置文件,除非此处另有说明。

  2. 首先,我们定义一个验证回调的配置数组。与前面的方法一样,每个回调应该实现Application\Filter\CallbackInterface,并且应该返回Application\Filter\Result的实例。验证器将采用以下通用形式:

    use Application\Filter\ { Result, Messages, CallbackInterface };
    $config = [
      // validator callbacks
      'validators' => [
        'key' => new class () implements CallbackInterface 
        {
          public function __invoke($item, $params) : Result
          {
            // validation logic goes here
            return new Result($valid, $error);
          }
        },
        // etc.
  3. 接下来,我们定义一个Application\Filter\Validator类,它在分配数组中循环,根据分配的验证器回调测试每个数据项。我们让这个类扩展AbstractFilter以提供前面描述的核心功能:

    namespace Application\Filter;
    class Validator extends AbstractFilter
    {
      // code
    }
  4. 在这个类中,我们定义了一个核心process()方法,该方法扫描一个数据数组,并根据分配数组应用验证器。如果没有为该数据集分配验证器,我们只需返回当前状态$valid(即TRUE

    public function process(array $data)
    {
      $valid = TRUE;
      if (!(isset($this->assignments) 
          && count($this->assignments))) {
            return $valid;
      }
  5. 否则,我们将$this->results初始化为Result对象数组,其中$item属性设置为TRUE,而$messages属性为空数组:

    foreach ($data as $key => $value) {
      $this->results[$key] = new Result(TRUE, array());
    }
  6. 然后我们复制一份$this->assignments并检查是否有任何全局过滤器(由“*键标识)。如果是这样,我们运行processGlobal(),然后取消设置“*键:

    $toDo = $this->assignments;
    if (isset($toDo['*'])) {
      $this->processGlobalAssignment($toDo['*'], $data);
      unset($toDo['*']);
    }
  7. 最后,我们循环通过任何剩余的任务,调用processAssignment()。这是检查数据中是否缺少 assignments 数组中的任何字段的理想位置。请注意,如果任何验证回调返回FALSE

    foreach ($toDo as $key => $assignment) {
      if (!isset($data[$key])) {
          $this->results[$key] = 
          new Result(FALSE, $this->missingMessage);
      } else {
          $this->processAssignment(
            $assignment, $key, $data[$key]);
      }
      if (!$this->results[$key]->item) $valid = FALSE;
      }
      return $valid;
    }

    ,我们将$valid设置为FALSE

  8. 正如您所记得的,每个赋值都被键入数据字段,并表示该字段的回调数组。因此,在processGlobalAssignment()中,我们需要循环调用数组。然而,在这种情况下,由于这些分配是全局,我们还需要循环遍历整个数据集,并依次应用每个全局过滤器。

  9. 与等价的Application\Filter\Fiter::processGlobalAssignment()方法相比,我们需要调用mergeValidationResults()。原因是,如果$result->item的值已经是FALSE,我们需要确保随后不会被TRUE的值覆盖。链中返回FALSE的任何验证器必须覆盖任何其他验证结果:

    protected function processGlobalAssignment($assignment, $data)
    {
      foreach ($assignment as $callback) {
        if ($callback === NULL) continue;
        foreach ($data as $k => $value) {
          $result = $this->callbacks[$callback['key']]
          ($value, $callback['params']);
          $this->results[$k]->mergeValidationResults($result);
        }
      }
    }
  10. 当我们定义processAssignment()时,以类似于processGlobalAssignment()的方式,我们需要执行分配给每个数据键的每个剩余回调,再次调用mergeValidationResults()

```php
protected function processAssignment($assignment, $key, $value)
{
  foreach ($assignment as $callback) {
    if ($callback === NULL) continue;
        $result = $this->callbacks[$callback['key']]
       ($value, $callback['params']);
        $this->results[$key]->mergeValidationResults($result);
    }
  }
```

它是如何工作的。。。

与前面的配方一样,确保定义以下类:

  • Application\Filter\Result
  • Application\Filter\CallbackInterface
  • Application\Filter\Messages
  • Application\Filter\AbstractFilter

您可以使用chap_06_post_data_config_messages.php文件,也在前面的配方中描述。

接下来,在Application\Filter文件夹中创建一个Validator.php文件。放置步骤 3 至 10 中所述的代码。

接下来,创建一个包含验证回调配置的chap_06_post_data_config_callbacks.php回调配置文件,如步骤 2 所述。每个回调都应遵循以下通用模板:

'validation_key' => new class () implements CallbackInterface 
{
  public function __invoke($item, $params) : Result
  {
    $error = array();
    $valid = /* perform validation operation on $item */
    if (!$valid) 
    $error[] = Messages::$messages['validation_key'];
    return new Result($valid, $error);
  }
}

现在您可以创建一个chap_06_post_data_validation.php调用脚本,该脚本初始化自动加载并包括配置脚本:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
include __DIR__ . '/chap_06_post_data_config_messages.php';
include __DIR__ . '/chap_06_post_data_config_callbacks.php';

接下来,定义分配数组,将数据字段映射到验证器回调键:

$assignments = [
  'first_name'       => [ ['key' => 'length',  
  'params'   => ['min' => 1, 'max' => 128]], 
                ['key' => 'alnum',   
  'params'   => ['allowWhiteSpace' => TRUE]],
                ['key'   => 'required','params' => []] ],
  'last_name'=> [ ['key' => 'length',  
  'params'   => ['min'   => 1, 'max' => 128]],
                ['key'   => 'alnum',   
  'params'   => ['allowWhiteSpace' => TRUE]],
                ['key'   => 'required','params' => []] ],
  'address'       => [ ['key' => 'length',  
  'params'        => ['max' => 256]] ],
  'city'          => [ ['key' => 'length',  
  'params'        => ['min' => 1, 'max' => 64]] ], 
  'state_province'=> [ ['key' => 'length',  
  'params'        => ['min' => 1, 'max' => 32]] ], 
  'postal_code'   => [ ['key' => 'length',  
  'params'        => ['min' => 1, 'max' => 16] ], 
                     ['key' => 'alnum',   
  'params'        => ['allowWhiteSpace' => TRUE]],
                     ['key' => 'required','params' => []] ],
  'phone'         => [ ['key' => 'phone', 'params' => []] ],
  'country'       => [ ['key' => 'in_array',
  'params'        => $countries ], 
                     ['key' => 'required','params' => []] ],
  'email'         => [ ['key' => 'email', 'params' => [] ],
                     ['key' => 'length',  
  'params'        => ['max' => 250] ], 
                     ['key' => 'required','params' => [] ] ],
  'budget'        => [ ['key' => 'float', 'params' => []] ]
];

对于测试数据,使用前面配方中描述的chap_06_post_data_filtering.php文件中定义的相同良好不良数据。之后,您可以创建一个Application\Filter\Validator实例,并测试数据:

$validator = new Application\Filter\Validator($config['validators'], $assignments);
$validator->setSeparator(PHP_EOL);
$validator->process($badData);
echo $validator->getMessageString(40, '%14s : %-26s' . PHP_EOL);
var_dump($validator->getItemsAsArray());
$validator->process($goodData);
echo $validator->getMessageString(40, '%14s : %-26s' . PHP_EOL);
var_dump($validator->getItemsAsArray());

正如所料,良好数据不会产生任何验证错误。另一方面,数据生成以下输出:

How it works...

通知该缺失字段addressstate_province验证FALSE,并返回缺失项消息。

将验证绑定到表单

第一次呈现表单时,将表单类(如前一配方中描述的Application\Form\Factory)绑定到可以执行过滤或验证的类(如前一配方中描述的Application\Filter\*)上的价值很小。然而,一旦提交了表单数据,人们的兴趣就会增加。如果表单数据未通过验证,则可以筛选值,然后重新显示。验证错误消息可以绑定到表单元素,并在表单字段旁边呈现。

怎么做。。。

  1. 首先,确保实现在实现表单工厂链接$\u POST 过滤器链接$\u POST 验证器配方中定义的类。

  2. 我们现在将注意力转向Application\Form\Factory类,并添加属性和 setter,它们允许我们附加Application\Filter\FilterApplication\Filter\Validator的实例。我们还需要定义$data,用于保留过滤和/或验证数据:

    const DATA_NOT_FOUND = 'Data not found. Run setData()';
    const FILTER_NOT_FOUND = 'Filter not found. Run setFilter()';
    const VALIDATOR_NOT_FOUND = 'Validator not found. Run setValidator()';
    
    protected $filter;
    protected $validator;
    protected $data;
    
    public function setFilter(Filter $filter)
    {
      $this->filter = $filter;
    }
    
    public function setValidator(Validator $validator)
    {
      $this->validator = $validator;
    }
    
    public function setData($data)
    {
      $this->data = $data;
    }
  3. 接下来,我们定义一个调用嵌入Application\Filter\Validator实例的process()方法的validate()方法。我们检查$data$validator是否存在。如果不是,则抛出相应的异常,并指示需要首先运行哪个方法:

    public function validate()
    {
      if (!$this->data)
      throw new RuntimeException(self::DATA_NOT_FOUND);
    
      if (!$this->validator)
      throw new RuntimeException(self::VALIDATOR_NOT_FOUND);
  4. 调用process()方法后,我们将验证结果消息与表单元素消息关联起来。注意,process()方法返回一个布尔值,表示数据集的整体验证状态。验证失败后重新显示表单时,每个元素旁边将显示错误消息:

    $valid = $this->validator->process($this->data);
    
    foreach ($this->elements as $element) {
      if (isset($this->validator->getResults()
          [$element->getName()])) {
            $element->setErrors($this->validator->getResults()
            [$element->getName()]->messages);
          }
        }
        return $valid;
      }
  5. 以类似的方式,我们定义了一个调用嵌入的Application\Filter\Filter实例的process()方法的filter()方法。与步骤 3 中描述的validate()方法一样,我们需要检查$data$filter是否存在。如果其中一个丢失,我们抛出一个带有相应消息的RuntimeException

    public function filter()
    {
      if (!$this->data)
      throw new RuntimeException(self::DATA_NOT_FOUND);
    
      if (!$this->filter)
      throw new RuntimeException(self::FILTER_NOT_FOUND);
  6. 然后我们运行process()方法,它生成一个Result对象数组,$item属性表示过滤器链的最终结果。然后,我们循环遍历结果,如果相应的$element键匹配,则将value属性设置为过滤值。我们还添加筛选过程中产生的任何消息。然后重新显示表单时,所有值属性将显示过滤结果:

    $this->filter->process($this->data);
    foreach ($this->filter->getResults() as $key => $result) {
      if (isset($this->elements[$key])) {
        $this->elements[$key]
        ->setSingleAttribute('value', $result->item);
        if (isset($result->messages) 
            && count($result->messages)) {
          foreach ($result->messages as $message) {
            $this->elements[$key]->addSingleError($message);
          }
        }
      }      
    }
    }

它是如何工作的。。。

您可以从开始对Application\Form\Factory进行如上所述的更改。对于测试目标,您可以使用工作原理中显示的 prospects 数据库表。。。链接$U 后过滤器配方的部分。各种列设置应该让您知道要定义哪些表单元素、过滤器和验证器。

例如,您可以定义一个chap_06_tying_filters_to_form_definitions.php文件,该文件将包含表单包装器、元素和过滤器分配的定义。以下是一些例子:

<?php
use Application\Form\Generic;

define('VALIDATE_SUCCESS', 'SUCCESS: form submitted ok!');
define('VALIDATE_FAILURE', 'ERROR: validation errors detected');

$wrappers = [
  Generic::INPUT  => ['type' => 'td', 'class' => 'content'],
  Generic::LABEL  => ['type' => 'th', 'class' => 'label'],
  Generic::ERRORS => ['type' => 'td', 'class' => 'error']
];

$elements = [
  'first_name' => [  
     'class'     => 'Application\Form\Generic',
     'type'      => Generic::TYPE_TEXT, 
     'label'     => 'First Name', 
     'wrappers'  => $wrappers,
     'attributes'=> ['maxLength'=>128,'required'=>'']
  ],
  'last_name'   => [  
    'class'     => 'Application\Form\Generic',
    'type'      => Generic::TYPE_TEXT, 
    'label'     => 'Last Name', 
    'wrappers'  => $wrappers,
    'attributes'=> ['maxLength'=>128,'required'=>'']
  ],
    // etc.
];

// overall form config
$formConfig = [ 
  'name'       => 'prospectsForm',
  'attributes' => [
'method'=>'post',
'action'=>'chap_06_tying_filters_to_form.php'
],
  'row_wrapper'  => ['type' => 'tr', 'class' => 'row'],
  'form_wrapper' => [
    'type'=>'table',
    'class'=>'table',
    'id'=>'prospectsTable',
    'class'=>'display','cellspacing'=>'0'
  ],
  'form_tag_inside_wrapper' => FALSE,
];

$assignments = [
  'first_name'    => [ ['key' => 'length',  
  'params'        => ['min' => 1, 'max' => 128]], 
                     ['key' => 'alnum',   
  'params'        => ['allowWhiteSpace' => TRUE]],
                     ['key' => 'required','params' => []] ],
  'last_name'     => [ ['key' => 'length',  
  'params'        => ['min' => 1, 'max' => 128]],
                     ['key' => 'alnum',   
  'params'        => ['allowWhiteSpace' => TRUE]],
                     ['key' => 'required','params' => []] ],
  'address'       => [ ['key' => 'length',  
  'params'        => ['max' => 256]] ],
  'city'          => [ ['key' => 'length',  
  'params'        => ['min' => 1, 'max' => 64]] ], 
  'state_province'=> [ ['key' => 'length',  
  'params'        => ['min' => 1, 'max' => 32]] ], 
  'postal_code'   => [ ['key' => 'length',  
  'params'        => ['min' => 1, 'max' => 16] ], 
                     ['key' => 'alnum',   
  'params'        => ['allowWhiteSpace' => TRUE]],
                     ['key' => 'required','params' => []] ],
  'phone'         => [ ['key' => 'phone',   'params' => []] ],
  'country'       => [ ['key' => 'in_array',
  'params'        => $countries ], 
                     ['key' => 'required','params' => []] ],
  'email'         => [ ['key' => 'email',   'params' => [] ],
                     ['key' => 'length',  
  'params'        => ['max' => 250] ], 
                     ['key' => 'required','params' => [] ] ],
  'budget'        => [ ['key' => 'float',   'params' => []] ]
];

您可以使用前面配方中描述的已有的chap_06_post_data_config_callbacks.phpchap_06_post_data_config_messages.php文件。最后,定义一个设置自动加载的chap_06_tying_filters_to_form.php文件,包括这三个配置文件:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
include __DIR__ . '/chap_06_post_data_config_messages.php';
include __DIR__ . '/chap_06_post_data_config_callbacks.php';
include __DIR__ . '/chap_06_tying_filters_to_form_definitions.php';

接下来,您可以创建表单工厂、筛选器和验证程序类的实例:

use Application\Form\Factory;
use Application\Filter\ { Validator, Filter };
$form = Factory::generate($elements);
$form->setFilter(new Filter($callbacks['filters'], $assignments['filters']));
$form->setValidator(new Validator($callbacks['validators'], $assignments['validators']));

然后您可以检查是否有$_POST数据。如果是,请执行验证和筛选:

$message = '';
if (isset($_POST['submit'])) {
  $form->setData($_POST);
  if ($form->validate()) {
    $message = VALIDATE_SUCCESS;
  } else {
    $message = VALIDATE_FAILURE;
  }
  $form->filter();
}
?>

视图逻辑非常简单:只需渲染表单。各种元素的任何验证消息和值都将作为验证和筛选的一部分进行分配:

  <?= $form->render($form, $formConfig); ?>

下面是一个使用坏表单数据的示例:

How it works...

请注意过滤和验证消息。还要注意错误的标记:

How it works...