PHP DevCenter
oreilly.comSafari Books Online.Conferences.

advertisement


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.


Valuable Online Certification Training

Online Certification for Your Career
Earn a Certificate for Professional Development from the University of Illinois Office of Continuing Education upon completion of each online certificate program.

PHP/SQL Programming Certificate — The PHP/SQL Programming Certificate series is comprised of four courses covering beginning to advanced PHP programming, beginning to advanced database programming using the SQL language, database theory, and integrated Web 2.0 programming using PHP and SQL on the Unix/Linux mySQL platform.

Enroll today!


Sponsored by: