# 创建通用表单元素生成器

要创建一个简单输出表单输入标签的函数是非常容易的，比如`<input type="text" name="whatever">`。然而，为了使表单生成器具有通用性，我们需要考虑更多的情况。下面是基本输入标签之外的一些其他考虑因素。

* 表单输入标签及其关联的HTML属性&#x20;
* 告诉用户他们正在输入什么信息的标签&#x20;
* 验证后显示输入错误的能力（稍后会介绍更多！）&#x20;
* 某种包装器，例如标记或HTML表标记

## 如何做...

1.首先，我们定义一个`Application\Form\Generic`类。这个类以后也将作为专门的表单元素的基类。

```php
namespace Application\Form;

class Generic
{
  // 一些代码 ...
}
```

2\. 接下来，我们定义了一些类常量，这些常量一般会在表单元素生成中使用。

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

```php
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\. 在这个例子中，我们需要两个属性，`$name`和`$type`，因为没有这些属性我们就不能有效地使用元素。其他构造参数是可选的。此外，为了使一个表单元素基于另一个表单元素，我们包含了一个规定，即第二个参数`$type`可以是`Application\Form\Generic`的一个实例，在这种情况下，我们只需运行`getters`（后面讨论）来填充属性。

```php
protected $name;
protected $type    = self::DEFAULT_TYPE;
protected $label   = '';
protected $errors  = array();
protected $wrappers;
protected $attributes;    // HTML 表单属性
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;
}
```

{% hint style="warning" %}
注意，`$wrappers`有三个主要的子键。`INPUT`, `LABEL`, 和`ERRORS`. 这允许我们为标签、输入标签和错误定义单独的包装器。
{% endhint %}

6\. 在定义为标签、输入标签和错误产生HTML的核心方法之前，我们应该定义一个 `getWrapperPattern()` 方法，该方法将为标签、输入和错误显示产生相应的包装标签。

7\. 例如，如果包装器被定义为`<div>`，并且它的属性包括`['class'=>'label']`，那么这个方法将返回一个`sprintf()`格式的模式，看起来像这样。`<div class="label">%s</div>`。例如，最终产生的HTML标签将替换`%s`。

8\. 下面是`getWrapperPattern()`方法的样子。

```php
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()`将标签插入到包装器中。

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

10\. 为了生成核心输入标签，我们需要有一种方法来组装属性。幸运的是，只要将它们以关联数组的形式提供给构造函数，就可以轻松实现这一点。在这种情况下，我们需要做的就是定义一个 `getAttribs()` 方法，该方法产生一个由键值对组成的字符串，并以空格分隔。我们使用`trim()`返回最终值，以去除多余的空格。

11\. 如果元素包含`value`或`href`属性，则出于安全原因，我们假定这些值是用户提供的（因此值得怀疑），所以我们需要转义。 因此，我们需要添加一个`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);
}
```

12\. 对于核心的输入标签，我们将逻辑分成两个独立的方法。主要方法`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());
}
```

13.现在我们定义一个显示元素验证错误的方法。我们假设错误是以数组的形式提供的，如果没有错误，我们返回一个空字符串。否则，错误将以`<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);
}
```

14\. 对于某些属性，我们可能需要对属性的各个方面进行更有限的控制。例如，我们可能需要在已经存在的错误数组中添加一个错误。另外，设置一个单一的属性也可能是有用的。

```php
public function setSingleAttribute($key, $value)
{
  $this->attributes[$key] = $value;
}
public function addSingleError($error)
{
  $this->errors[] = $error;
}
```

15\. 最后，我们定义了`getter`和`setter`，允许我们检索或设置属性的值。例如，你可能已经注意到`$pattern`的默认值是`<input type="%s" name="%s" %s>`。对于某些标签（例如，选择和表单标签），我们需要将此属性设置为不同的值。

```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;
}
// 定义类似的获取和设置方法
// 用于名称、标签、包装物、错误和属性。
```

16\. 我们还需要添加能给出标签值（不是HTML）的方法，以及错误数组。

```php
public function getLabelValue()
{
  return $this->label;
}
public function getErrorsArray()
{
  return $this->errors;
}
```

## 如何运行...

请确保将前面所有的代码复制到一个单一的`Application\Form\Generic`类中。然后你可以定义一个`chap_06_form_element_generator.php`调用脚本来设置自动加载和锚定新类。

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

接下来，定义包装器。为了说明问题，我们将使用HTML表格数据和头标签。注意标签使用`TH`，而输入和错误使用`TD`。

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

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

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

或者，使用设置器定义密码元素。

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

最后，一定要定义一个提交按钮。

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

实际的显示逻辑可能是这样的。

```php
<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>
```

下面是实际输出。

![](/files/-MJw-8BiKTDYgZnBVyCD)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ganymedenil.gitbook.io/php-7/mo-kuai-yi/di-liu-zhang-jian-li-ke-kuo-zhan-de-wang-zhan/chuang-jian-tong-yong-biao-dan-yuan-su-sheng-cheng-qi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
