Autofilled PHP Forms
Pages: 1, 2, 3
Tidying Up
The first version of fillInFormValues() put its arguments into global variables so the callback functions that do the real work could get to them. Yuck. All those callback functions clutter up the PHP function namespace, too; double yuck.
There's a straightforward fix--encapsulate the arguments and callback functions in a "helper" class, and then use the array(&$this, "function") callback syntax supported by all the PHP functions that have callback arguments. fillInFormValues() creates a helper object, then calls a method on that object to do all the work:
function fillInFormValues($formHTML, $request = null, $formErrors = null)
{
if ($request === null) {
// magic_quotes on: gotta strip slashes:
if (get_magic_quotes_gpc()) {
function stripslashes_deep(&$val) {
$val = is_array($val) ? array_map('stripslashes_deep', $val)
: stripslashes($val);
return $val;
}
$request = stripslashes_deep($_REQUEST);
}
else {
$request = $_REQUEST;
}
}
if ($formErrors === null) { $formErrors = array(); }
$h = new fillInFormHelper($request, $formErrors);
return $h->fill($formHTML);
}
/**
* Helper class, exists to encapsulate info needed between regex callbacks.
*/
class fillInFormHelper
{
var $request; // Normally $_REQUEST, passed into constructor
var $formErrors;
var $idToNameMap; // Map form element ids to names
function fillInFormHelper($r, $e)
{
$this->request = $r;
$this->formErrors = $e;
}
function fill($formHTML)
{
$s = fillInFormHelper::getTagPattern('input');
$formHTML = preg_replace_callback("/$s/is",
array(&$this, "fillInInputTag"), $formHTML);
// Using simpler regex for textarea/select/label, because in practice
// they never have >'s inside them:
$formHTML = preg_replace_callback('!(<textarea([^>]*>))(.*?)(</textarea\s*>)!is',
array(&$this, "fillInTextArea"), $formHTML);
$formHTML = preg_replace_callback('!(<select([^>]*>))(.*?)(</select\s*>)!is',
array(&$this, "fillInSelect"), $formHTML);
// Form errors: tag <label> with class="error", and fill in
// <ul class="error"> with form error messages.
$formHTML = preg_replace_callback('!<label([^>]*)>!is',
array(&$this, "fillInLabel"), $formHTML);
$formHTML = preg_replace_callback('!<ul class="error">.*?</ul>!is',
array(&$this, "getErrorList"), $formHTML);
return $formHTML;
}
/**
* Returns pattern to match given a HTML/XHTML/XML tag.
* NOTE: Setup so only the whole expression is captured
* (subpatterns use (?: ...) so they don't catpure).
* Inspired by http://www.cs.sfu.ca/~cameron/REX.html
*
* @param string $tag E.g. 'input'
* @return string $pattern
*/
function getTagPattern($tag)
{
$p = '('; // This is a hairy regex, so build it up bit-by-bit:
$p .= '(?is-U)'; // Set options: case-insensitive, multiline, greedy
$p .= "<$tag"; // Match <tag
$sQ = "(?:'.*?')"; // Attr val: single-quoted...
$dQ = '(?:".*?")'; // double-quoted...
$nQ = '(?:\w*)'; // or not quoted at all, but no wacky characters.
$attrVal = "(?:$sQ|$dQ|$nQ)"; // 'value' or "value" or value
$attr = "(?:\s*\w*\s*(?:=$attrVal)?)"; // attribute or attribute=
$p .= "(?:$attr*)"; // any number of attr=val ...
$p .= '(?:>|(?:\/>))'; // End tag: > or />
$p .= ')';
return $p;
}
/**
* Returns value of $attribute, given guts of an HTML tag.
* Returns false if attribute isn't set.
* Returns empty string for no-value attributes.
*
* @param string $tag Guts of HTML tag, with or without the <tag and >.
* @param string $attribute E.g. "name" or "value" or "width"
* @return string|false Returns value of attribute (or false)
*/
function getAttributeVal($tag, $attribute) {
$matches = array();
// This regular expression matches attribute="value" or
// attribute='value' or attribute=value or attribute
// It's also constructed so $matches[1][...] will be the
// attribute names, and $matches[2][...] will be the
// attribute values.
preg_match_all('/(\w+)((\s*=\s*".*?")|(\s*=\s*\'.*?\')|(\s*=\s*\w+)|())/s',
$tag, $matches, PREG_PATTERN_ORDER);
for ($i = 0; $i < count($matches[1]); $i++) {
if (strtolower($matches[1][$i]) == strtolower($attribute)) {
// Gotta trim off whitespace, = and any quotes:
$result = ltrim($matches[2][$i], " \n\r\t=");
if ($result[0] == '"') { $result = trim($result, '"'); }
else { $result = trim($result, "'"); }
return $result;
}
}
return false;
}
/**
* Returns new guts for HTML tag, with an attribute replaced
* with a new value. Pass null for new value to remove the
* attribute completely.
*
* @param string $tag Guts of HTML tag.
* @param string $attribute E.g. "name" or "value" or "width"
* @param string $newValue
* @return string
*/
function replaceAttributeVal($tag, $attribute, $newValue) {
if ($newValue === null) {
$pEQv = '';
}
else {
// htmlspecialchars here to avoid potential cross-site-scripting attacks:
$newValue = htmlspecialchars($newValue);
$pEQv = $attribute.'="'.$newValue.'"';
}
// Same regex as getAttribute, but we wanna capture string offsets
// so we can splice in the new attribute="value":
preg_match_all('/(\w+)((\s*=\s*".*?")|(\s*=\s*\'.*?\')|(\s*=\s*\w+)|())/s',
$tag, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE);
for ($i = 0; $i < count($matches[1]); $i++) {
if (strtolower($matches[1][$i][0]) == strtolower($attribute)) {
$spliceStart = $matches[0][$i][1];
$spliceLength = strlen($matches[0][$i][0]);
$result = substr_replace($tag, $pEQv, $spliceStart, $spliceLength);
return $result;
}
}
if (empty($pEQv)) { return $tag; }
// No match: add attribute="newval" to $tag (before closing tag, if any):
$closed = preg_match('!(.*?)((>|(/>))\s*)$!s', $tag, $matches);
if ($closed) {
return $matches[1] . " $pEQv" . $matches[2];
}
return "$tag $pEQv";
}
/**
* Returns modified <input> tag, based on values in $request.
*
* @param array $matches
* @return string Returns new guts.
*/
function fillInInputTag($matches) {
$tag = $matches[0];
$type = fillInFormHelper::getAttributeVal($tag, "type");
if (empty($type)) { $type = "text"; }
$name = fillInFormHelper::getAttributeVal($tag, "name");
if (empty($name)) { return $tag; }
$id = fillInFormHelper::getAttributeVal($tag, "id");
if (!empty($id)) { $this->idToNameMap[$id] = $name; }
switch ($type) {
/*
* Un-comment this out at your own risk (users shouldn't be
* able to modify hidden fields):
* case 'hidden':
*/
case 'text':
case 'password':
if (!array_key_exists($name, $this->request)) {
return $tag;
}
return fillInFormHelper::replaceAttributeVal($tag, 'value', $this->request[$name]);
break;
case 'radio':
case 'checkbox':
$value = fillInFormHelper::getAttributeVal($tag, "value");
if (empty($value)) { $value = "on"; }
if (strpos($name, '[]')) {
$name = str_replace('[]', '', $name);
}
if (!array_key_exists($name, $this->request)) {
return fillInFormHelper::replaceAttributeVal($tag, 'checked', null);
}
$vals = (is_array($this->request[$name])?$this->request[$name]:array($this->request[$name]));
if (in_array($value, $vals)) {
return fillInFormHelper::replaceAttributeVal($tag, 'checked', 'checked');
}
return fillInFormHelper::replaceAttributeVal($tag, 'checked', null);
}
return $tag;
}
/**
* Returns modified <textarea...> tag, based on values in $request.
*
* @param array $matches
* @return string Returns new value.
*/
function fillInTextArea($matches) {
$tag = $matches[1]; // The <textarea....> tag
$val = $matches[3]; // Stuff between <textarea> and </textarea>
$endTag = $matches[4]; // The </textarea> tag
$name = fillInFormHelper::getAttributeVal($tag, "name");
if (empty($name)) { return $matches[0]; }
$id = fillInFormHelper::getAttributeVal($tag, "id");
if (!empty($id)) { $this->idToNameMap[$id] = $name; }
if (!array_key_exists($name, $this->request)) { return $matches[0]; }
return $tag.htmlspecialchars($this->request[$name]).$endTag;
}
/**
* Returns modified <option value="foo"> tag, based on values in $vals.
*
* @param array $matches
* @return string Returns tag with selected="selected" or not.
*/
function fillInOption($matches)
{
$tag = $matches[1]; // The option tag
$valueAfter = $matches[2]; // Potential value (stuff after option tag)
$val = fillInFormHelper::getAttributeVal($tag, "value");
if (empty($val)) { $val = trim($valueAfter); }
if (in_array($val, $this->selectVals)) {
return fillInFormHelper::replaceAttributeVal($tag, 'selected', 'selected').$valueAfter;
}
else {
return fillInFormHelper::replaceAttributeVal($tag, 'selected', null).$valueAfter;
}
}
var $selectVals;
/**
* Returns modified <select...> tag, based on values in $request.
*
* @param array $matches
* @return string
*/
function fillInSelect($matches) {
$tag = $matches[1];
$options = $matches[3];
$endTag = $matches[4];
$name = fillInFormHelper::getAttributeVal($tag, "name");
if (empty($name)) { return $matches[0]; }
$id = fillInFormHelper::getAttributeVal($tag, "id");
if (!empty($id)) { $this->idToNameMap[$id] = $name; }
if (strpos($name, '[]')) {
$name = str_replace('[]', '', $name);
}
if (!array_key_exists($name, $this->request)) { return $matches[0]; }
$this->selectVals = (is_array($this->request[$name])?$this->request[$name]:array($this->request[$name]));
// Handle all the various flavors of:
// <option value="foo" /> OR <option>foo</option> OR <option>foo
$s = fillInFormHelper::getTagPattern('option');
$pat = "!$s(.*?)(?=($|(</option)|(</select)|(<option)))!is";
$options = preg_replace_callback($pat, array(&$this, "fillInOption"), $options);
return $tag.$options.$endTag;
}
/**
* Returns modified <label...> tag, based on $formErrors.
*
* @param array $matches
* @return string
*/
function fillInLabel($matches) {
$tag = $matches[0];
$for = fillInFormHelper::getAttributeVal($tag, "for");
if (empty($for) or !isset($this->idToNameMap[$for])) { return $tag; }
$name = $this->idToNameMap[$for];
if (array_key_exists($name, $this->formErrors)) {
return fillInFormHelper::replaceAttributeVal($tag, 'class', 'error');
}
return $tag; // No error.
}
/**
* Returns modified <ul class="error"> list with $formErrors error messages.
*
* @return string
*/
function getErrorList() {
$result = "";
foreach (array_unique($this->formErrors) AS $f => $msg) {
if (!empty($msg)) {
$result .= "<li>".htmlspecialchars($msg)."</li>\n";
}
}
if (empty($result)) { return ""; } // No errors: return empty string.
$result = '<ul class="error">'.$result.'</ul>';
return $result;
}
} // End of helper class.
The Other Way
Several packages can help you generate HTML for forms from your PHP code. (I've heard nice things about HTML_QuickForm, for example.) Most of them also automate form validation and redisplay. Still, I don't like using PHP to generate HTML; I like separating application logic (PHP code) from display (HTML) as much as possible. It's very nice to be able to edit the look of a web page, including any forms on the page, in a WYSIWYG editor like DreamWeaver.
Simple Is Best
fillInFormValues() has a very simple interface--it's just a function call. The implementation isn't terribly complicated, either. It's under 400 lines of code, including comments. I like simple things; they're easier to integrate into bigger projects. I use fillInFormValues() to prepopulate forms with values fetched from a database. I register it as a Smarty block function, so forms in my page templates redisplay themselves properly. Since I've started using it, I haven't been tempted to take up plumbing.
Download the Source
All the source code for this article, plus unit tests and source for a fillInFormValues Smarty extension, are available for download as a .zip archive.
Gavin Andresen spends his time writing core content management system code for Gravity Switch, creating online games for the blind to play with each other and their sighted friends and family at All inPlay, and playing with his children.
Return to the PHP DevCenter.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 15 of 15.
-
Another bug fix
2007-03-15 09:13:19 GavinAndresen [Reply | View]
Thanks to Ulrich for finding this one:
Radio buttons or checkboxes with values of "0" weren't working properly (I made the common mistake of using the php empty() function where I shouldn't have). Code at skypaint.com/gavin/code has been updated.
-
Updated code, new example!
2007-03-06 16:05:50 GavinAndresen [Reply | View]
I finally got around to modifying fillInFormValues() to work with the PHP name="foo[]" syntax; updated code is at: http://www.skypaint.com/gavin/code/
There's a new example there that uses the array syntax for multiple text/radio/checkbox/select and textarea fields.
Also, <INPUT TYPE="RADIO"...> HTML 4-type uppercase-attribute syntax was broken, but now works properly.
-
Problem with using array input names
2006-07-06 23:47:19 sdave1284 [Reply | View]
Hey great script! This will save so much time. Now I just have to go back and get all my old scripts to use it. I did run into one problem and I wanted to see if you may have a solution....
If you have input fields like the following:
<input type="text" name="phone1[1]" id="phone1[1]">-
<input type="text" name="phone2[1]" id="phone2[1]">-
<input type="text" name="phone3[1]" id="phone3[1]">
It will not fill in the values. I guess it doesnt recognize how to fill in an array of values with <? echo $_POST['phone1'][1] ?>, etc....
Any ideas how I could modify the script or if its possible?? -
Problem with using array input names
2006-07-07 16:12:06 GavinAndresen [Reply | View]
I was wondering when somebody would notice that it doesn't handle PHP's built-in support for arrays of form fields.
On a scale of 1-10, that will be about a '6' to fix; it will involve changes to the fillInInputTag() and fillInTextArea() (and maybe fillInSelect) functions, where they fetch the "name" attribute (gotta be smarter about name="foo[]", and keep track of which array index is being parsed, and find the corresponding value in $request). -
Problem with using array input names
2006-07-11 00:21:32 sdave1284 [Reply | View]
Well, I don't use it everywhere. I only use it for text fields. So I made the following addition to fillInInputTag()(Note: i'm not very experienced with the best solutions to these problems, nor do I use regular expressions often):
$num_of_matches = preg_match("/([\w]+)(\W([\d]+)\W)/", $name, $name_match);
$name_arr = $name_match[1];
$name_index =$name_match[3];
if($num_of_matches == 0) {
if (!array_key_exists($name, $this->request))
return $tag;
return fillInFormHelper::replaceAttributeVal($tag, 'value', $this->request[$name]);
} else {
if (!array_key_exists($name_arr, $this->request))
return $tag;
return fillInFormHelper::replaceAttributeVal($tag, 'value', $this->request[$name_arr][$name_index]);
}
In my input names, I add a index number instead of just using name[]. I think this gives me better control, plus I can use the above method to fill in the text fields. Now, I just need to figure out how to do the same thing for the validateForm function....
Your scripts have made me life creating version 2 of our website 100X easier. Expansion will be much easier as new fields are added to the forms and database backend! Thanks and feel free to comment on what I did above...
Dave
-
Preselected radio buttons do not work
2006-04-27 07:53:39 jashnu [Reply | View]
Thanks for the good code. However, I noticed that if a form has radio button with checked=checked the selection drops the first time you enter the form. Any solutions? -
Preselected radio buttons do not work
2006-04-28 16:32:30 GavinAndresen [Reply | View]
Radio buttons are a pain, because "not checked" is represented by a LACK of a value in $_REQUEST.
So fillInFormValues is doing the right thing-- if you pass it an empty array of values, it clears their checked="checked".
To fix, you could either NOT pass your form through fillInFormValues() the very first time, OR you could pass in an array with the right stuff in it the very first time. Code would be something like:
// Only call fillInFormValues if NOT first time:
if (!empty($_POST)) {
$formHTML = fillInFormValues($formHTML, $_POST);
}
OR:
// First time: pass in correct checkbox states:
if (empty($_POST)) {
$values = array('checkbox1' => 1, 'checkbox2' => 1);
}
else {
$values = $_POST;
}
$formHTML = fillInFormValues($html, $values);
-
Seperating PHP from HTML
2006-04-27 04:09:01 JuddMuir [Reply | View]
I'm about to try your code for the form-filling, as I like the simplicity of the idea. You said that you like separating application logic (PHP code) from display (HTML) as much as possible - I'd like to apply this principle to the webpage as a whole.
Normally my PHP classes have a render method that generates the HTML, as I am doing the site top to bottom. However, on the project I am currently working on, I'd like to be able to let a designer do the HTML and CSS separately. So my question is, what have you found to be the best method to keep HTML out of PHP?
-
Cross-site-scripting (XSS) security hole...
2006-03-25 04:58:32 GavinAndresen [Reply | View]
There's a security hole in the short example: $_SERVER['PHP_SELF'] should be htmlspecialchars($_SERVER['PHP_SELF']) to prevent cross-site-scripting attackes.
I've updated the examples in the .zip file. A good description of the attack can be found at:
http://blog.phpdoc.info/archives/13-XSS-Woes.html
-
HTML parsing
2006-03-16 19:16:54 mpeters [Reply | View]
I'm not a PHP guy, but I know that parsing HTML with regular expressions is asking for problems. Does PHP not have any decent HTML parsers?
In Perl we use HTML::FillInForm, which is much more generic and uses HTML::Parser behind the scenes to handle all the oddities of HTML and do it very fast. It's as easy as:
HTML::FillInForm->new->fill(
file => 'blah.html',
fdat => { email => 'me@test.com' },
);
And with HTML::FillInForm::LibXML being written, it will use the libxml C library to make it blazingly fast. -
HTML parsing
2006-03-16 19:29:31 GavinAndresen [Reply | View]
PHP's got several HTML parsers (I mention HTML_Sax in the article); I used regular expressions instead because:
1) All the full-blown HTML parsers have dependencies on other stuff, and the more dependencies the more pain for me (we deploy our code at LOTS of different ISPS, with lots of different PHP/Apache/IIS configurations).
2) The HTML parsers assume your form is HTML. We use Smarty templates instead of raw HTML, and I use the same code to populate the form with either Smarty template variables or values from $_REQUEST.
-
HTML parsing
2006-06-07 11:52:56 sbrown29 [Reply | View]
Thank you so much. I had been looking for a form that redisplayed my form with the errors. In addition I wanted to email the form. I'm using this statement but it's not working
"$firstname = $values['fname']; echo $firstname". fname is a fieldname in the parent form. For some reason $firstname is always blank. Any suggestions?



Upgrade your version of pcre (painful, because you've got to recompile php) to fix. Or downgrade back to a version of php that doesn't use pcre7.0, and wait for a version of PHP that contains the fixed PCRE.