It's pretty easy to create a function that simply outputs a form input tag such as <input type="text" name="whatever" >. In order to make a form generator generically useful, however, we need to think about the bigger picture. Here are some other considerations over and above the basic input tag:
input tag and its associated HTML attributes<div> tag, or an HTML table <td> tagApplication\Form\Generic class. This will also later serve as a base class for specialized form elements:namespace Application\Form;
class Generic
{
// some code ...
}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';
$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;
}getWrapperPattern() method, which will produce the appropriate wrapping tags for the label, input, and error display.<div>, and its attributes include ['class' => 'label'], this method will return a sprintf() format pattern that looks like this: <div class="label">%s</div>. The final HTML produced for the label, for example, would then replace %s.getWrapperPattern() method might look: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;
}getLabel() method. All this method needs to do is to plug the label into the wrapper using sprintf():public function getLabel()
{
return sprintf($this->getWrapperPattern(self::LABEL),
$this->label);
}input tag, we need a way to assemble the attributes. Fortunately, this is easily accomplished as long as they are supplied to the constructor in the form of an associative array. All we need to do, in this case, is to define a getAttribs() method that produces a string of key-value pairs separated by a space. We return the final value using trim() to remove excess spaces.value or href attribute, for security reasons we should escape the values on the assumption that they are, or could be, user-supplied (and therefore suspect). Accordingly, we need to add an if statement that checks and then uses htmlspecialchars() or urlencode():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);
}getInputOnly(), produces only the HTML input tag. The second method, getInputWithWrapper(), produces the input embedded in a wrapper. The reason for the split is that when creating spin-off classes, such as a class to generate radio buttons, we will not need the wrapper:public function getInputOnly()
{
return sprintf($this->pattern, $this->type, $this->name,
$this->getAttribs());
}
public function getInputWithWrapper()
{
return sprintf($this->getWrapperPattern(self::INPUT),
$this->getInputOnly());
}<ul><li>error 1</li><li>error 2</li></ul> and so on: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);
}public function setSingleAttribute($key, $value)
{
$this->attributes[$key] = $value;
}
public function addSingleError($error)
{
$this->errors[] = $error;
}$pattern is <input type="%s" name="%s" %s>. For certain tags (for example, select and form tags), we will need to set this property to a different value: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 attributespublic function getLabelValue()
{
return $this->label;
}
public function getErrorsArray()
{
return $this->errors;
}Be sure to copy all the preceding code into a single Application\Form\Generic class. You can then define a chap_06_form_element_generator.php calling script that sets up autoloading and anchors the new class:
<?php require __DIR__ . '/../Application/Autoload/Loader.php'; Application\Autoload\Loader::init(__DIR__ . '/..'); use Application\Form\Generic;
Next, define the wrappers. For illustration, we'll use HTML table data and header tags. Note that the label uses TH, whereas input and errors use TD:
$wrappers = [ Generic::INPUT => ['type' => 'td', 'class' => 'content'], Generic::LABEL => ['type' => 'th', 'class' => 'label'], Generic::ERRORS => ['type' => 'td', 'class' => 'error'] ];
You can now define an email element by passing parameters to the constructor:
$email = new Generic('email', Generic::TYPE_EMAIL, 'Email', $wrappers,
['id' => 'email',
'maxLength' => 128,
'title' => 'Enter address',
'required' => '']);Alternatively, define the password element using setters:
$password = new Generic('password', $email);
$password->setType(Generic::TYPE_PASSWORD);
$password->setLabel('Password');
$password->setAttributes(['id' => 'password',
'title' => 'Enter your password',
'required' => '']);Lastly, be sure to define a submit button:
$submit = new Generic('submit',
Generic::TYPE_SUBMIT,
'Login',
$wrappers,
['id' => 'submit','title' => 'Click to login','value' => 'Click Here']);The actual display logic might look like this:
<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>Here is the actual output:
