I am writing this class to render HTML forms. It is working as expected, so far. There are many things to be done yet (including testing). Is it ok to keep this way?
/**
*
* This class renders (or aims to render) a valid HTML form
*
* Note: this documentation still needs to be completed. For now I think it does its job.
*
* Condensed usage:
*
* $form = new Form(["name"=>"myform", "method"=>"post", "action" =>"action.php", "id"=>"html_id"]);
* $form->add_field(["fieldset"]);
* $form->add_field(["label", ["value"=>"My Label", "for"=>"txtareaname"]]);
* $form->add_field(["textarea", ["id"=>"txtareaid", "name"=>"txtareaname", "cols"=>80, "rows"=>10, "value"=>"Texto del textarea"]]);
* $form->add_field(["fieldset"]);
* $form->add_field(["select",["name"=>"myselect"], ["value1"=>"text1", "value2"=>"text2"]]);
* $form->add_field(["text", ["name"=>"mytext", "value"=>"", "required"=>""]]);
* $form->add_field(["fieldset"]);
* $form->add_field(["label", ["for"=>"mycheck", "value"=>"My check?"]]);
* $form->add_field(["checkbox", ["name"=>"mycheck", "value"=>"mycheck", "id"=>"mycheck"]]);
* $form->add_field(["fieldset"]);
* $form->add_field(["submit", ["name"=>"enviar", "value"=>"enviar"]]);
* echo $form->render(); // or $output = $form->render();
*
*
* When this class is instantiated it takes a key-value array where the keys are any valid <form> attribute and the values
* are its corresponding attribute value. There are 3 mandatory attributes: name, method and action.
* Any other attribute, if present, must have a value, so it cannot be an empty string. So for a basic usage:
*
* $form = new Form(["name"=>"myform", "method"=>"post", "action"=>"destination.php", "id"=>"form_id"]);
* echo $form->render();
*
* Should output something similar to:
*
* <form name="myform" action="destination.php" method="post" id="form_id">
*
* </form>
*
* For adding fields add_field() is used. It takes a multidimensional array (keys are ignored) where the first
* element (key [0]) is the HTML form input tag name (button, select, fieldset, etc.).
* The second element (optional in some cases) is a key-value array where the keys are any valid attribute for that HTML
* element
* and the value its corresponding value. Optionally, it can take a 3rd element when a select or optgroup is inserted.
* This 3rd element is also a key-value array where the keys are option's value attribute and the values are the text
* to show.
*
* As well as <form> each element may have its own mandatory attributes and they will be checked.
*
* To add a text field:
*
* $form = new Form(["name"=>"myform", "method"=>"post", "action"=>"destination.php"]);
* $form->add_field(["input", ["type"=>"text", "name"=>"mytext", "required"=>true, "value"=>"bool(true)"]]);
* $form->add_field(["input", ["type"=>"text", "name"=>"myothertext", "required"=>"cheese tax", "value"=>"cheese tax"]]);
* echo $form->render();
*
* Should output something like:
*
* <form name='myform' action='destination.php' method='post'>
* <input type='text' name='mytext' value='bool(true)' required>
* <input type='text' name='myothertext' value='cheese tax' required>
* </form>
*
* This example below should render something like the example above. Notice the add_field() method:
*
* $form = new Form(["name"=>"myform", "method"=>"post", "action"=>"destination.php"]);
* $form->add_field(["text", ["name"=>"mytext", "required"=>""], "value"=>"string(0)"]);
* $form->add_field(["text", ["name"=>"myothertext", "required"=>null, "value"=>null]]);
* echo $form->render();
*
* Output:
*
* <form name='myform' action='destination.php' method='post'>
* <input type='text' name='mytext' value='string(0)' required>
* <input type='text' name='myothertext' value='' required>
* </form>
*
*
* As <input> or <button> can be "tricky" due to the amount of types it is allowed to use the type of input
* as (pseudo) element.
*
* Note that with boolean attributes (such as required or autofocus), if present, no matter its value, will be added.
*
* To insert a <select>:
*
* $form->add_field(["select",["name"=>"control_select"], ["value1"=>"text1", "value2"=>"text2"]]);
*
* To insert a <label> bind to a <textarea>:
*
* $form->add_field(["label", ["value"=>"My label", "for"=>"txtareaname"]]);
* $form->add_field(["textarea", ["id"=>"txtareaid", "name"=>"txtareaname", "cols"=>80, "rows"=>10, "value"=>"Texto del textarea"]]);
*
* In the cases of labels and textareas the value attribute is considered as the text to show.
*
* To insert a <fieldset>:
*
* $form->add_field(["fieldset"]);
* $form->add_field(["label", ["value"=>"My label", "for"=>"txtareaname"]]);
* $form->add_field(["textarea", ["id"=>"txtareaid", "name"=>"txtareaname", "cols"=>80, "rows"=>10, "value"=>"Texto del textarea"]]);
*
* Take into consideration that fieldsets are closed in render() method. So if you need to add several fieldsets:
*
* $form->add_field(["fieldset"]);
* $form->add_field(["label", ["value"=>"Etiqueta", "for"=>"txtareaname"]]);
* $form->add_field(["textarea", ["id"=>"txtareaid", "name"=>"txtareaname", "cols"=>80, "rows"=>10, "value"=>"Texto del textarea"]]);
* $form->add_field(["fieldset"]);
* $form->add_field(["select",["name"=>"control_select"], ["value1"=>"text1", "value2"=>"text2"]]);
* $form->add_field(["text", ["name"=>"mytext", "value"=>"", "required"=>""]]);
* $form->add_field(["fieldset"]);
* $form->add_field(["label", ["for"=>"mycheck", "value"=>"My check?"]]);
* $form->add_field(["checkbox", ["name"=>"mycheck", "value"=>"mycheck", "id"=>"mycheck"]]);
* $form->add_field(["fieldset"]);
* $form->add_field(["submit", ["name"=>"enviar", "value"=>"enviar"]]);
*
*
* This class produces (or at least aims to produce) a valid HTML form block so when an error occurs an exception is raised.
* Errors that may occur are incomplete data fields, missing mandatory attributes (or its values)
* or trying to use an illegal form element.
*
* There are certain attributes like role or event handlers for example that are not supported yet.
* Custom or non-standard attributes are not allowed yet. It is planned for the future.
* Also it does not verifies for valid attribute's values (will be added in the future).
* Javascript is not allowed yet. It is planned for the future. However, I have been told that javascript should be handled via event listeners, so....
* Nested forms or subforms are not allowed.
* No sanitization is done as this class outputs HTML code straight for echoing so it is not safe to be stored in
* a database and user input validation or any other kind of validation is up to the developer.
*
* Arrays for handling the form elements and its attributes could be optimized. I did not want to have infinite
* multidimensional arrays and I think it is easier to maintain and to read as it is. It may require some extra
* if and foreach statements this way.
*
* In the future I plan to add strict and non-strict operation modes to allow flexibility and more customization.
*
* In the meantime I need to improve what I have until now. I still need to polish elements special treatment, error
* handling, array optimization and logic.
*
* Should other HTML tags such as <br>, <p>, and others be allowed? If I allow them, then I should think bigger than
* just a class for HTML forms.... I just wanted a simple class to render forms
*
*
*/
class Form {
/** in here each add_field() valid output is stored. it is looped in render() */
private $output_elements = array();
private $form_elements = ['input', 'label', 'select', 'textarea', 'button', 'fieldset', 'legend',
'datalist', 'output', 'option', 'optgroup'];
private $form_elements_types = array(
'input' => ['button', 'checkbox', 'color', 'date', 'datetime-local', 'email', 'file', 'hidden', 'image', 'month',
'number', 'password', 'radio', 'range', 'reset', 'search', 'submit', 'tel', 'text', 'time', 'url', 'week'],
'button' => ['submit', 'reset', 'button'],
);
private $form_elements_common_attr = ['id', 'class', 'accesskey', 'style', 'tabindex'];
private $form_elements_required_attr = array(
'form' => ['name', 'action', 'method'],
'select' => ['name'],
'textarea' => ['name', 'cols', 'rows'],
'button' => ['name', 'value'],
'option' => ['value', 'text'],
'input' => ['type', 'name', 'value'],
);
private $form_elements_specific_attr = array(
'form' => ['target', 'enctype', 'autocomplete', 'rel', 'novalidate'],
'label' => ['for'],
'select' => ['autocomplete', 'autofocus', 'disabled', 'form', 'multiple', 'required', 'size'],
'textarea' => ['autocomplete', 'autofocus', 'disabled', 'form', 'maxlength', 'minlength', 'placeholder', 'readonly', 'required'],
'button' => ['autofocus', 'disabled', 'form', 'formaction', 'formenctype', 'formmethod', 'formtarget', 'formnovalidate'],
'fieldset' => ['disabled', 'form'],
'legend' => [],
'datalist' => [],
'output' => ['for', 'form'],
'option' => ['disabled', 'label', 'selected'],
'optgroup' => ['disabled', 'label'],
'input' => ['disabled', 'required'],
'checkbox' => ['checked'],
'color' => [],
'date' => ['max', 'min', 'step'],
'datetime-local' => ['max', 'min', 'step'],
'email' => ['list', 'maxlength', 'minlength', 'multiple', 'pattern', 'placeholder', 'readonly', 'size'],
'file' => ['accept', 'capture', 'multiple'],
'hidden' => [],
'image' => ['alt', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'height', 'src', 'width'],
'month' => ['list', 'max', 'min', 'readonly', 'step'],
'number' => ['list', 'max', 'min', 'placeholder', 'readonly', 'step'],
'password' => ['maxlength', 'minlength', 'pattern', 'placeholder', 'readonly', 'size'],
'radio' => ['checked', 'required'],
'range' => ['list', 'max', 'min', 'step'],
'reset' => [],
'search' => ['list', 'maxlength', 'minlength', 'pattern', 'placeholder', 'readonly', 'size', 'spellcheck'],
'submit' => ['formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget'],
'tel' => ['list', 'maxlength', 'minlength', 'pattern', 'placeholder', 'readonly', 'size'],
'text' => ['list', 'maxlength', 'minlength', 'pattern', 'placeholder', 'readonly', 'size', 'spellcheck'],
'time' => ['list', 'max', 'min', 'readonly', 'step'],
'url' => ['list', 'maxlength', 'minlength', 'pattern', 'placeholder', 'readonly', 'size', 'spellcheck'],
'week' => ['max', 'min', 'readonly', 'step']
);
private $form_elements_forbidden_attr = array(); // to implement in the future
private $form_tag_closing = ['label', 'select', 'textarea', 'legend', 'option'];
/**
*
* @param array $form Asociative array where the keys are the form tag attributes and the values the attribute's value
* @throws Exception
*
* Attributes name, action and method are mandatory and cannot be empty.
* The rest of the attributes, if set must have a valid value.
*
* Usage:
*
* $form = new Form(['name'=>'myform', 'action'=>'action.php', 'method'=>'post']);
*
* Result:
*
* <form name='myform' action='action.php' method='post'>
*
*/
function __construct(array $form) {
$output = "<form";
if (empty($form)) throw new Exception("Form data cannot be empty");
// this foreach could be implemented in, for example, check_required_attr() method... don't figure out if useful
foreach($this->form_elements_required_attr['form'] as $attr) {
if (!array_key_exists($attr, $form)) throw new Exception("Form attribute '$attr' is mandatory");
if (empty($form[$attr])) throw new Exception("Form attribute '$attr' value cannot be an empty string");
$output .= " $attr='$form[$attr]'";
}
foreach($this->form_elements_specific_attr['form'] as $attr) {
if (array_key_exists($attr, $form)) {
if (empty($form[$attr])) throw new Exception("Form attribute '$attr' cannot be an empty string");
$output .= " $attr='$form[$attr]'";
}
}
foreach($this->form_elements_common_attr as $attr) {
if (array_key_exists($attr, $form)) {
if (empty($form[$attr])) throw new Exception("Form attribute '$attr' cannot be an empty string");
$output .= " $attr='$form[$attr]'";
}
}
$output .= ">";
$this->output_elements[] = $output;
}
function add_field(array $data) {
$output = null;
$element = null;
$type = null;
$attributes = null;
$options = null;
if (empty($data)) throw new Exception("Field data cannot be empty");
if (count($data) < 1) throw new Exception("Field data should be an array of at least one element");
$element = $data[array_keys($data)[0]]; // done this way in case user uses associative array. Should I issue a warning relative to function's parameter format?
if (empty($element) or !is_string($element)) throw new Exception("Form element's name should contain only letters."); // maybe here I should use a regexp. O maybe delete it as later on is checked against an array... however, I consider it an important advice
// attributes
if (count($data) >= 2) {
$attributes = $data[array_keys($data)[1]];
if (!is_array($attributes)) throw new Exception("Field attributes (2nd element in parameter array) should be an (empty) array");
}
// options for select, optgroup, etc.
if (count($data) >= 3) {
$options = $data[array_keys($data)[2]];
if (!is_array($options)) throw new Exception("Options parameter (3rd element in parameter array) should be an array");
if (count($options) < 1) throw new Exception("Options cannot be empty"); // maybe yes (for JS, x ex)?
}
// check for allowed controls considering "trickyness" on input or button types
$found = false;
if (!in_array($element, $this->form_elements)) {
foreach ($this->form_elements_types as $s_element => $s_types) {
if (in_array($element, $s_types)) {
$type = $element;
$element = $s_element;
$found = true;
break;
}
}
} else { $found = true; }
if (!$found) throw new Exception("'$element': illegal form (type) element");
unset($found);
$output .= "<$element";
if (array_key_exists($element, $this->form_elements_required_attr)) {
foreach($this->form_elements_required_attr[$element] as $attr) {
// input or button
if (($attr == "type") and !is_null($type)) { // I have doubts on this validation... needs a better check
$output .= " type='$type'";
} else {
if (!array_key_exists($attr, $attributes)) {
// yes yes, messages need to be handled better... I'll get to that later
if (!is_null($type)) throw new Exception("'$element $type' attribute '$attr' is mandatory");
throw new Exception("'$element attribute '$attr' is mandatory");
}
// value is the only attribute that can be an empty string but it's mandatory
if (($attr != "value") and empty($attributes[$attr])) {
if (!is_null($type)) throw new Exception("'$element $type' attribute '$attr' cannot be empty");
throw new Exception("$element attribute '$attr' cannot be empty");
}
$output .= " $attr='".$attributes[$attr]."'";
}
}
}
// element has specific non-mandatory attributes
if (array_key_exists($element, $this->form_elements_specific_attr) and (!is_null($attributes))) {
foreach($this->form_elements_specific_attr[$element] as $attr) {
if (array_key_exists($attr, $attributes)) {
if (($attr == "required") or ($attr == "autofocus")) $output .= " $attr";
else $output .= " $attr='" . $attributes[$attr] . "'";
}
}
}
if (!is_null($attributes)) {
foreach($this->form_elements_common_attr as $attr) {
if (array_key_exists($attr, $attributes) and !empty($attributes[$attr])) $output .= " $attr='" . $attributes[$attr] . "'";
}
}
$output .= ">";
if ($element == "select") {
foreach($options as $value => $text) $output .= "\n<option value='$value'>$text</option>";
}
// in this cases (textarea, label, etc.) pseudo-attribute value is used as the element's text
if (($element != "input") and ($element != "select")) $output .= $attributes['value'];
if (in_array($element, $this->form_tag_closing)) $output .= "</$element>";
$this->output_elements[] = $output;
}
function render() {
$output = null;
$open_fieldsets = 0;
foreach($this->output_elements as $element) {
// to handle fieldsets I thought of adding a special attribute to: ["fieldset", ["close"=>true]], but not sure
if (strpos($element, "fieldset") !== false) {
if ($open_fieldsets > 0) {
$output .= "\n</fieldset>";
$open_fieldsets--;
}
$open_fieldsets++;
}
$output .= "\n".$element;
}
if ($open_fieldsets != 0) $output .= "\n</fieldset>";
return $output . "\n</form>\n";
// return "\n".implode("\n", $this->output_elements)."\n</form>";
}
}
Besides trying to write as standard as I am able I am trying to write something that someone else would use or at least, hopefully, use as a good starting example to more advanced developers.