I've been trying to figure out a "good way" to handle displaying and validating forms.
My aims were to allow: simple and quick definition of the form, validation of fields, "popuation" of fields, treat all input fields the same.
This is what I've come up with so far - I would like to hear your thoughts.
Forms are defined in XML, an example:
The form is validated and populated by an interestingly titled Form class:Code:<?xml version="1.0"?> <capsule describes="form"> <title>Login</title> <intro>Access members only content and features.</intro> <form action="Login_Form"> <fieldset> <legend>Login Details</legend> <input name="username" populateMultipleValues="no" submitMultipleValues="no" required="yes" type="text"> <label>Username</label> <populator class="Cookie_FormPopulator" params="name:login_username"/> <validator class="Validation_AlphaNumericValidator"/> <validator class="Validation_EmailValidator"/> </input> <input name="password" populateMultipleValues="no" submitMultipleValues="no" required="yes" type="secret"> <label>Password</label> <populator class="Cookie_FormPopulator" params="name:login_password"/> <filter class="Filter_ProfanityFilter"/> <validator class="Validation_AlphaNumericValidator"/> </input> </fieldset> </form> </capsule>
And an example usage would be:PHP Code:class Form {
/**
* Pepper_XPath object representing the Form Descriptor
*
* @access private
* @var object Pepper_XPath
*/
protected $formXPath;
/**
* DomDocument object representing the Form Descriptor
*
* @access private
* @var object DomDocument
*/
protected $formDoc;
/**
* Hash containing the names and values of fields that passed validation.
*
* @access protected
* @var array
*/
protected $validValues;
/**
* Denotes whether the form failed validation.
*
* @access protected
* @var boolean
*/
protected $failedValidation;
/**
* Constructor
*
* @access public
* @param object DomDocument
* @return void
*/
public function __construct(DomDocument $formDoc) {
$this->formDoc = $formDoc;
$this->formXPath = new Pepper_XPath($this->formDoc);
$this->validValues = array();
$this->valid = TRUE;
$this->dataSet = array();
} // __construct()
/**
* Populates input fields with values.
*
* @access public
* @return void
*/
public function populate() {
foreach ($this->dataSet as $key => $value) {
// Populate fields with original values.
if ($result = $this->formXPath->query('/capsule/form/fieldset/input[@name="'.$key.'"]')) {
if ($node = $result->item(0)) {
$node->appendChild(new DomElement('value', stripslashes(htmlentities($value, ENT_QUOTES))));
}
}
}
foreach ($this->formXPath->query('/capsule/form/fieldset/input/populator') as $populatorNode) {
$input = $populatorNode->parentNode;
if (isset($this->dataSet[$input->getAttribute('name')])) {
// We've already populated this above.
continue;
}
$class = $populatorNode->getAttribute('class');
$populator = new $class;
if ($populatorNode->hasAttribute('params')) {
foreach (Pepper_Util::extractParams($populatorNode->getAttribute('params')) as $key => $value) {
$method = 'set' . $key;
$populator->$method($value);
}
}
$values = $populator->getValues();
if ($input->getAttribute('populateMultipleValues') == 'no' && count($values) > 1) {
// Error... populator returned too many values.
} else {
foreach ($values as $key => $value) {
$value = $input->appendChild(new DomElement('value', $value));
$value->setAttribute('key', $key);
}
}
}
} // populate()
/**
* Validates each input field in the form.
*
* Validation rules are retrieved from the form descriptor.
*
* @access public
* @param array DataSet containing the values to validate against.
* @return void
*/
public function validate($dataSet) {
$this->dataSet = $dataSet;
if (!is_array($dataSet)) {
throw (new Form_ValidationException('DataSet (argument 1 to validate()) must be an array.'));
}
foreach ($this->formXPath->query('/capsule/form/fieldset/input') as $input) {
$inputName = $input->getAttribute('name');
if ((!isset($dataSet[$inputName]) || empty($dataSet[$inputName])) && $input->getAttribute('required') == 'yes') {
$err = $input->appendChild(new DomElement('error'));
$err->setAttribute('type', 'required');
$this->valid = FALSE;
continue;
}
if (isset($dataSet[$inputName]) & !empty($dataSet[$inputName])) {
if ($input->getAttribute('submitMultipleValues') == 'no' && is_array($dataSet[$inputName])) {
// Error... multiple values can't be submitted for this input.
$this->valid = FALSE;
continue;
}
$validation = new Validation();
$validation->setValue($dataSet[$inputName]);
foreach (Pepper_XmlUtil::getElementsByTagName($input, 'validator') as $validatorNode) {
$class = $validatorNode->getAttribute('class');
$validation->addValidator(new $class);
}
if (!$validation->success()) {
$this->valid = FALSE;
foreach ($validation->getErrors() as $err) {
$input->appendChild(new DomElement('error', $err));
}
} else {
$this->validValues[$inputName] = $dataSet[$inputName];
}
}
}
if (!$this->valid) {
throw (new Form_ValidationException());
}
} // validate()
/**
* Returns DataSet of valid values.
*
* @access public
* @return array
*/
public function getValues() {
return $this->validValues;
} // getValues()
} // Form
The form definition is used by the Form class to control validation and population of fields. The Form class alters the form definition as it encounters invalid fields, or retrieves values to populate fields with. The form definition the becomes the output document, to be transformed using XSLT.PHP Code:class Login_Form implements Action, Response_DocumentProducer {
protected $data;
protected $form;
protected $capsule;
protected $responseValid;
public function __construct() {
$dfa = new Application_DataFileAccess($this);
$ptr = $dfa->locate('login-form-definition.xml');
$ptr->setAdapter(new Pepper_DomDocument());
$this->data = $ptr->getFile();
$this->form = new Form($this->data);
$this->responseValid = TRUE;
$this->capsule = new Response_Document($this);
// TODO: Get placement and template to use using Application_Settings
$this->capsule->setPlacement('content');
$this->capsule->setTemplate('html/minimal-slick/Form/form.xml');
} // __construct()
public function process() {
if (isset($_POST['__action__']) && $_POST['__action__'] == get_class($this)) {
try {
$this->form->validate($_POST);
$processor = new Login_Processor();
$processor->setValues($this->form->getValues());
try {
$processor->process();
// What to do?
// Show confirmation: "Thanks, you are now logged in."
// Or display nothing?
} catch (Login_ProcessorException $e) {
// Show error.
$this->responseValid = FALSE;
}
} catch (Form_ValidationException $e) {
try {
$this->form->populate();
} catch (Form_ValidationException $e) {
// Show error.
$this->responseValid = FALSE;
}
}
} else {
try {
$this->form->populate();
} catch (Form_ValidationException $e) {
// Show error.
$this->responseValid = FALSE;
}
}
} // process()
public function getData() {
return $this->data;
} // getData()
public function responseIsValid() {
return $this->responseValid;
} // responseIsValid()
} // Login_Form
Populators are an attempt to make adding values to select, radio elements quickly.
If you think I've overlooked something, or if you think the design is wrong, or anything else, let me know.







Bookmarks