PHP DevCenter

oreilly.comSafari Books Online.Conferences.

We've expanded our LAMP news coverage and improved our search! Search for all things LAMP across O'Reilly!

Search
Search Tips

advertisement

Listen Print Discuss Subscribe to PHP Subscribe to Newsletters

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.


Have a similar technique or a refinement? Let's talk about it.
You must be logged in to the O'Reilly Network to post a talkback.
Post Comment
Full Threads Oldest First

Showing messages 1 through 15 of 15.

  • Beware: fillInFormValues affected by pcre7 bug
    2007-07-03 11:33:02  GavinAndresen [Reply | View]

    If you upgrade to php5.5.2/pcre7.0, and fillInFormValues stops working, you've get a version of pcre (7.0) that doesn't like one of the regular expressions used by fillInFormValues.

    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.
    • Beware: fillInFormValues affected by pcre7 bug
      2007-07-18 10:18:24  Dunn-PHP [Reply | View]

      I think you mean PHP 5.2.2 (not 5.5.2). I can confirm this behaviour on a Linux/Apache installation where the code seems to just abort prematurely and a blank page is delivered.
  • 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?
    • Separating PHP from HTML
      2006-04-28 16:25:39  GavinAndresen [Reply | View]

      We use Smarty templates extensively at Gravity Switch; just about all the output stuff (HTML pages, text of email messages) is put into Smarty templates and fetched by the PHP code.
  • 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?


Tagged Articles

Be the first to post this article to del.icio.us

Sponsored Resources

  • Inside Lightroom
Advertisement

Sponsored by:

O'Reilly Media

©2009, O'Reilly Media, Inc.
(707) 827-7000 / (800) 998-9938
All trademarks and registered trademarks appearing on oreilly.com are the property of their respective owners.
About O'Reilly
Academic Solutions
Authors
Contacts
Customer Service
Jobs
Newsletters
O'Reilly Labs
Press Room
Privacy Policy
RSS Feeds
Terms of Service
User Groups
Writing for O'Reilly
Content Archive
Business Technology
Computer Technology
Google
Microsoft
Mobile
Network
Operating System
Digital Photography
Programming
Software
Web
Web Design
More O'Reilly Sites
O'Reilly Radar
Ignite
Tools of Change for Publishing
Digital Media
Inside iPhone
O'Reilly FYI
makezine.com
craftzine.com
hackszine.com
perl.com
xml.com

Partner Sites
InsideRIA
java.net
O'Reilly Insights on Forbes.com