SitePoint Sponsor |
|
User Tag List
Results 1 to 19 of 19
Thread: A good way to handle forms?
Hybrid View
-
Mar 3, 2004, 08:34 #1
- Join Date
- Sep 2003
- Location
- Bristol, UK
- Posts
- 145
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
A good way to handle forms?
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:
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>
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
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.Last edited by sleepeasy; Mar 3, 2004 at 09:18.
Always open to question or ridicule
-
Mar 3, 2004, 09:42 #2
- Join Date
- May 2003
- Location
- Berlin, Germany
- Posts
- 1,829
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Looks really cool, don't see any design flaws. Might like to look at PEAR's Quickform to get some inspiration.
-
Mar 3, 2004, 10:40 #3
- Join Date
- Jan 2004
- Location
- Planet Earth
- Posts
- 1,764
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Nice
very clean, though I use XML as well, I use it for validation only since my templates are all xHTML, and not XML - I'm assuming you use XML for your templates as well ?
Not so clean though, is my current solution. Your script looks cool - like how you've used XPATH - again
PHP Code:<?xml version="1.0" encoding="iso-8859-1"?>
<module>
<forms>
<form name='LogIn'>
<username>
<error_msg />
<regexp>/^[a-zA-Z0-9]+$/</regexp>
</username>
<password>
<error_msg />
<regexp>/^[0-9]+$/</regexp>
</password>
</form>
</forms>
...PHP Code:class FormValidation {
var $posted;
var $errors;
var $values;
function FormValidation( $conf, $form ) {
$this -> errors = array();
$this -> posted = isset( $_POST['BasicSubmit'] )? true:false;
#
$this -> Prepare( $conf, $form );
}
function IsValid() {
foreach( $this -> errors as $error ) {
if( empty( $error[0] ) ) {
return false;
}
}
return true;
}
function IsSubmitted() {
return $this -> posted;
}
function Fetch( $key ) {
return $this -> values[$key];
}
function Validate( $f, $value ) {
# validate a given form text (text|password|textarea) element
if( !preg_match( $f['regexp'], $value ) ) {
return array( false, $f['error_msg'] );
}
else {
return array( true, $value );
}
}
function Prepare( $conf, $form ) {
$count = 0;
$xml = $conf -> FindChildrenOf( 'form', $form );
#
foreach( $_POST as $key => $value ) {
if( $key !== (string) 'BasicSubmit' /* exclude */ ) {
$tmp = strtolower( $key );
$this -> values[$key] = $value; // temp solution, should read from 'errors' array :)
$this -> errors[$count] = $this -> Validate( $conf -> Extract( $xml[$tmp] ), $value );
}
$count++;
}
}
}
class XmlWriter { }
class XmlReader {
# PHP4.3.4 IIS5.1
var $dom;
var $indexer;
function XmlReader( $filename ) {
if( !file_exists( $filename ) ) {
ErrorReport::Invoke( 200 );
die();
}
$this -> dom = domxml_open_file( $filename );
}
function Extract( $elements ) {
$index = array();
foreach( $elements -> child_nodes() as $element ) {
if( $element -> node_type() === XML_ELEMENT_NODE ) {
$index[$element -> tagname()] = $element -> get_content();
}
}
return $index;
}
function NameOf( $node ) {
return $node -> tagname();
}
function TypeOf( $node ) {
return $node -> node_type();
}
function ContentOf( $node ) {
return $node -> get_content();
}
function AttributeOf() { }
function FindChildrenOf( $tag, $attr ) {
$items = $this -> dom -> get_elements_by_tagname( $tag );
$index = array();
#
foreach( $items as $item ) {
$attribute = $item -> get_attribute( 'name' );
#
if( $attribute == (string) $attr ) {
$children = $item -> child_nodes();
#
foreach( $children as $child ) {
if( $child -> node_type() === XML_ELEMENT_NODE ) {
$index[$child -> tagname()] = $child;
}
}
}
}
return $index;
}
}
PHP Code:$conf = &new XmlReader( 'form.action.xml' );
$f = &new FormValidation( $conf, 'LogIn' );
#
if( !$f -> IsSubmitted() ) {
// not sent
}
else if( !$f -> IsValid() ) {
// sent, but bad input formats
}
else {
// all inputs are fine
}
-
Mar 3, 2004, 11:57 #4
- Join Date
- Sep 2003
- Location
- Bristol, UK
- Posts
- 145
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by DarkAngelBGE
Originally Posted by Widow Maker
The form XML I posted serves two purposes, it defines the rules for validation etc. and it's also the document that is sent to the XSLT processor.
Originally Posted by Widow Maker
Originally Posted by Widow Maker
Off Topic:
PHP Code:ErrorReport::Invoke( 200 );
Was wondering if you could explain what your ErrorReport object does or how you handle errors in general?
Always open to question or ridicule
-
Mar 3, 2004, 13:03 #5
- Join Date
- May 2003
- Location
- Berlin, Germany
- Posts
- 1,829
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
I suggest you build some little "How-To-Use-This" document and sell the script.
Oh and don't forget to send us here at SP a free copy beforehand.
-
Mar 3, 2004, 13:35 #6
- Join Date
- Sep 2003
- Location
- Bristol, UK
- Posts
- 145
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by DarkAngelBGE
Nice idea but I don't think it would be very popular because PHP5 isn't stable yet, and it will be part of my... wait for it... framework (yes, another one
) which will be open source.
Most of it will be (must be) finished in 3 weeks - I'm going to use it for an E-Commerce website that my girlfriend and her best friend are setting up.Always open to question or ridicule
-
Mar 3, 2004, 13:11 #7
- Join Date
- Jan 2004
- Location
- Planet Earth
- Posts
- 1,764
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
...RegEx's in object's that can be used in a number of forms,...
As to
PHP Code:ErrorReport::Invoke( [int] );
I've a few things working together, but finding the time to glue things together is a problem
At the moment, the Integer passed over, will determine a default error to be outputted, though nothing else, such as what caused an error yes ?
A solution to this I was thinking is that, in every class I have that would require error checking, I have a basic Fetch() which would return a more descriptive message - or other object - to what caused the error.
This way, the ErrorReport class wouldn't really know, nor care about where the message - or object - came from.
It'd just use it yes ? Here is what I have at the moment though,. Thanks for your comments SleepEasy, and would be interested in seeing what you think of my ideas for ErrorReport.
PHP Code:class ErrorReport {
function ErrorReport() { }
function Invoke( $e ) {
ob_flush();
$index = array(
'100' => 'Error - Unable to make a connection to database services.',
'101' => 'Error - Unable to complete database query.',
'200' => 'Error - Unable to access required file.' );
require_once( ERROR_PAGE );
die();
}
}
Another point is, I'm thinking of using PHPs own logging facitlities - a good idea ?
-
Mar 3, 2004, 13:17 #8
- Join Date
- Jan 2004
- Location
- Planet Earth
- Posts
- 1,764
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
ie - off the top of my head on this one.
PHP Code:...
function IsError() {
return 'Use of class XmlReader caused an error';
// example :)
}
function XmlReader( $filename ) {
if( !file_exists( $filename ) ) {
ErrorReport::Invoke( 200, &$this );
die();
}
$this -> dom = domxml_open_file( $filename );
}
Something like that anyway.
-
Mar 3, 2004, 14:00 #9
- Join Date
- Sep 2003
- Location
- Bristol, UK
- Posts
- 145
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
WidowMaker, Thanks for showing me your code.
Some observations:
Your error system looks good, but I would suggest you make ErrorReport "dumber" and only have one default error message to use when IsError() returns nothing or fails, instead of one for each error code. Because even with those few you enforce ErrorReport to know about what causes errors.
Something like "Sorry, we encountered an error fulfilling your request, please try again in x minutes." or something.
Edit:
I just remembered you said the default errors will eventually be taken from an XML file - if this is updated when you add new error producing "modules" then what I said above won't apply as much. Sorry about that.
I just think ErrorReport shouldn't know about what the error is or could be, just how bad the error is.
You could then use the error code to represent the severity of the error which would be used by ErrorReport to determine whether the error should be logged, an email sent to the admin, or both.Always open to question or ridicule
-
Mar 3, 2004, 14:35 #10
- Join Date
- Jan 2004
- Location
- Planet Earth
- Posts
- 1,764
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Good points SleepEasy, used PHPs own error reporting before, though apparently PHPs own model nevel crossed me mind
Thanks. I'll see what I can do, and post back soonDo you think it'd be difficult to implement a custom logger, much like you'd implement a custom sesion handler ?
ie Using PHPs own logging functions, but instead, handling the actual logging yourself ?
Bit of a brain drain just now
-
Mar 3, 2004, 20:38 #11
- Join Date
- Sep 2003
- Location
- Bristol, UK
- Posts
- 145
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
I have only ever written a very simple log writer class so I'm not an expert on it the subject, but I can't imagine it being to difficult, although that obviously depends on what exactly you want doing.
Just have a goAlways open to question or ridicule
-
Mar 3, 2004, 22:37 #12
- Join Date
- Sep 2003
- Location
- Glasgow
- Posts
- 1,690
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
After skimming quickly through that, it appears you are using hard-coded form definitions: can your system cope with a form with a variable number of form fields? With some forms, you don't always know in advance exactly what the field list will be - hence you would need to define the form meta data dynamically.
-
Mar 4, 2004, 06:05 #13
- Join Date
- Sep 2003
- Location
- Bristol, UK
- Posts
- 145
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by McGruff
If you needed you could dynamically create the form definition, or append new inputs to an existing one. That would work fine as long as you did this before calling the populate() and validate() Form methods.
I still have a few things to do before I'm happy with the class and I might add something to make it easy to work with the types of forms you're referring to.
Thanks for the comment.Always open to question or ridicule
-
Mar 4, 2004, 07:07 #14
- Join Date
- Sep 2003
- Location
- Bristol, UK
- Posts
- 145
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Just thought I should mention that the code I posted isn't finished - so don't use it
Always open to question or ridicule
-
Mar 4, 2004, 07:47 #15
- Join Date
- Sep 2003
- Location
- Glasgow
- Posts
- 1,690
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by sleepeasy
Also, you need to consider what happens if the data used to calculate a variable fields list is changed by someone/something else while you're busy entering data & submitting a form. With the forum example, say another admin has added or deleted a user group while you are adding a new board.
If the form meta data is defined at the time the form is processed it will be incorrect - the original form was created with different fields. Validation could produce a false positive since meta data doesn't list all the POST keys. If you check submissions for alien POST keys, they would always fail to validate.
The solution might be to define the form meta data when the form is first displayed. When the form is processed, check the definition is still valid before proceeding.
That's maybe a bit of an obscure and unlikely use-case but worth bearing in mind.
-
Mar 3, 2004, 22:32 #16
- Join Date
- Jan 2004
- Location
- Planet Earth
- Posts
- 1,764
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
I was wanting the log primarlary to be dumped to a database, though the option of a flat file would be nice as well
Then there is another idea to dump the whole lot to XML, for archiving and printing purposes, ie so you could use a number of different XSL stylesheets to output a varied amount of detail yes ?
Basic display, date/time, error for example. More detail would be the user who happened to be using the application at the time, etc etc Think you get the idea anyways
-
Mar 4, 2004, 00:13 #17
- Join Date
- Jan 2004
- Location
- Planet Earth
- Posts
- 1,764
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
...it appears you are using hard-coded form definitions...
If mine, then yes, you do have a point. From SleepEasy's script and the XML file - as it's actually the document (page) it's self, there may be a way of dynamically altering the INPUT being transformed dynamically I suppose.
As for me, at the moment I'm not too concerned about dynamic FORMs. For the moment
One issue I can think of at the moment is how a dynamic element would be generated yes ?
From server-side, or via client-side Javascript and the DOM.
Off the top of my head, the Form Validation that I have doesn't really (it's self) care about what constitutes a FORM, all it needs is the RegExp and Error tags on a per Form element basis.
These could be generated on the fly, what concerns me is how to generate the actual FORM then ?
-
Mar 6, 2004, 15:41 #18
- Join Date
- Jan 2004
- Location
- Planet Earth
- Posts
- 1,764
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
You could then use the error code to represent the severity of the error which would be used by ErrorReport to determine whether the error should be logged, an email sent to the admin, or both.
PHP Code:...
ErrorReport::Invoke( 'ErrorCodeHere' );
die();
...
PHP Code:class ErrorReport {
...
function Invoke( $code ) {
$e = &new ErrorReporter( $code );
$e -> Report();
...
# display error page w/ message
}
}
class ErrorReporter {
...
function ErrorReporter( $code ) {
switch( $code ) {
case '100':
# critical error
return new Emailer;
break;
case '200':
return new Logger;
break;
...
default:
return new Logger;
break;
}
}
}
Bookmarks