phpDocumentor SForm
[ class tree: SForm ] [ index: SForm ] [ all elements ]

Source for file SForm.php

Documentation is available at SForm.php

  1. <?php
  2. /**
  3.  * A fast & powerful PEAR_QuickForm replacement
  4.  *
  5.  * SForm is short for Seth's Form. Why? Because it's easy to remember, and even
  6.  * easier to type. SForm was started out of frusteration with PEAR's QuickForm.
  7.  * It was too slow, it wouldn't allow me to create complex rules, the code was
  8.  * antiquidated, it didn't nest groups, the API was convoluted, the list went
  9.  * on. After spending a few days wrestling with QF, I came to the realization
  10.  * that I could code my own package in the time it took me to figure out how to
  11.  * bend QuickForm to my will. So that's what I did.
  12.  * 
  13.  * SForm automatically does the standard QF things, like {@link }
  14.  * SForm_Rule::toJavaScript() JavaScript generation}, rendering plugins, both
  15.  * server-side and {@link SForm_Element::validate() client side validation},
  16.  * and is very customizable. Some things not included in QF include non-
  17.  * enforced <b>XHTML 1.1 compliance</b>, <b>{@link SForm_Elm_Select::lock()}
  18.  * intrinsic rules}</b>, <b>nested groups</b>, and <b>{@link }
  19.  * http://www.webstandards.org/learn/tutorials/accessible-forms/beginner/
  20.  * automatic accessibility features}</b> like
  21.  * <label> tags, optgroups, JavaScript highlighting of errors, and access keys.
  22.  * I've tried to keep the API very similar to QuickForm, but I've removed things
  23.  * that I found unnecessary or redundant.
  24.  * 
  25.  * SForm is also <i>fast</i>.
  26.  * 
  27.  * The small form that I originally used to develop SForm had a render time of
  28.  * 0.10-0.28 seconds when no form was created. Using QuickForm to build a form
  29.  * on the page increased the render time to 0.33-0.68 secs. Replacing QuickForm
  30.  * with SForm dropped the render time to 0.18-0.42 secs. SForm is almost
  31.  * <b>three times faster</b> than QuickForm in this informal test. (All trials
  32.  * used eAccelerator with caching and optimization enabled.)
  33.  * 
  34.  * <b>Example</b>:
  35.  * 
  36.  * The SForm API is intentionally very similar to QuickForm. This example was
  37.  * converted directly from the {@link http://pear.php.net/manual/en/package.}
  38.  * html.html-quickform.tutorial.php QuickForm documentation}. All of the
  39.  * examples on {@link http://www.midnighthax.com/quickform.php Keith Edmunds's}
  40.  * QuickForm tutorial} also work with similar modifications. It should be noted
  41.  * that to make this example XHTML 1.1 compliant, the elements would need to be
  42.  * grouped in a fieldset.
  43.  * 
  44.  * <code>
  45.  * <?php
  46.  * // Load the main classes
  47.  * require_once 'SForm.php';
  48.  * 
  49.  * // Instantiate the SForm object
  50.  * $form = new SForm('firstForm');
  51.  * 
  52.  * // Set defaults for the form elements
  53.  * $form->setDefaults(array( 'name' => 'Joe User' ));
  54.  * 
  55.  * // Add some elements to the form
  56.  * $form->addElement('header', null, 'SForm tutorial example');
  57.  * $form->addElement('text', 'name', 'Enter your name:',
  58.  *                     array('size' => 50, 'maxlength' => 255));
  59.  * $form->addElement('submit', null, 'Send');
  60.  * 
  61.  * // Define filters and validation rules
  62.  * $form->applyFilter('name', 'trim');
  63.  * $form->addRuleDep('name', 'Please enter your name', 'required', null,'client');
  64.  * 
  65.  * // Try to validate a form
  66.  * if ($form->validate()) {
  67.  *     echo       '<h1>Hello, ' . htmlspecialchars($form->exportValue('name')) . '!</h1>';
  68.  *     exit;
  69.  * }
  70.  * 
  71.  * // Output the form
  72.  * $form->display();
  73.  * ?>
  74.  * </code>
  75.  *
  76.  *
  77.  * Note that in the example, addRule() has been changed to {@link }
  78.  * SForm::addRuleDep() addRuleDep()}, where 'Dep' stands for
  79.  * depriciated. For more information, see the {@link SForm_Rule}
  80.  * SForm_Rule documentation}.
  81.  * 
  82.  * <b>Putting it all together</b>:
  83.  * 
  84.  * {@example  SForm_usage.php}
  85.  * Look in SForm_usage.php for this source
  86.  * 
  87.  * <b>Important differences with the QuickForm API include</b>:
  88.  * 
  89.  * - The construtor. The $target attribute {@link SForm::__construct() has been removed} because it breaks compliance  with XHTML 1.1.
  90.  * - Rule creation and usage. SForm uses a much more object oriented {@link SForm_Rule Rule model}.
  91.  * 
  92.  * <b>Requirements</b>:
  93.  * 
  94.  * SForm requires at least PHP version 5, but version 5.1 is recommended.
  95.  *
  96.  * <b>License</b>:
  97.  * 
  98.  * Copyright (c) 2006, Seth Price <{@link mailto:seth@pricepages.org seth@pricepages.org}> All rights reserved.
  99.  *
  100.  * Redistribution and use in source and binary forms, with or without
  101.  * modification, are permitted provided that the following conditions
  102.  * are met:
  103.  *
  104.  * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  105.  * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  106.  * - The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.
  107.  *
  108.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  109.  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  110.  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  111.  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  112.  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  113.  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  114.  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  115.  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
  116.  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  117.  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  118.  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  119.  *
  120.  * @copyright    Copyright (c) 2006, Seth Price
  121.  * @author        Seth Price <seth@pricepages.org>
  122.  * @license        http://opensource.org/licenses/bsd-license.php New BSD License
  123.  * @access        public
  124.  * @package        SForm
  125.  * @see            SForm::__construct()
  126.  * @version        0.1.2
  127.  */
  128.  
  129. /**
  130.  * Document with:
  131.  * ./phpdoc -t ../docs -f ../scripts/SForm.php -d ../scripts/SForm -ti "SForm" -o HTML:frames:phpdoc.de
  132.  * 
  133.  * @ignore
  134.  */
  135.  
  136. /**
  137.  * FIXME - clean up exception messages and values.
  138.  * @deprecated
  139.  * @ignore
  140.  */
  141. define('FORM_ERR_PARENT_EXISTS'1);
  142.  
  143. /**
  144.  * Create the count interface if needed
  145.  * 
  146.  * The Countable interface is not enabled by default in PHP 5.0, so we'll
  147.  * enable it.
  148.  * 
  149.  * @ignore
  150.  */
  151. if(!interface_exists('Countable')){
  152.     interface Countable{
  153.         public function count();
  154.     }
  155. }
  156.  
  157. /**
  158.  * A element is a "leaf" that is actually rendered
  159.  * 
  160.  * All elements and containers are decendents of this class. That said, this
  161.  * class is intended to cover as much common functionality as possible.
  162.  * 
  163.  * @access        public
  164.  * @package        SForm
  165.  */
  166. abstract class SForm_Element {
  167.     
  168.     /**
  169.      * This is the element's tag ('input', 'select', 'form', etc) and should be
  170.      * set by the children as needed.
  171.      * 
  172.      * @var        string 
  173.      */
  174.     protected $tag = 'input';
  175.     
  176.     /**
  177.      * Is this element's tag empty?
  178.      * 
  179.      * A tag is empty if it can't contain elements. Examples of empty tags
  180.      * include '<input />' and '<img />'. Non-empty tags include '<form>' and
  181.      * '<fieldset>'.
  182.      * 
  183.      * @var        boolean 
  184.      */
  185.     protected $emptyTag = true;
  186.     
  187.     /**
  188.      * Should we add the 'name' attribute?
  189.      * 
  190.      * @var        boolean 
  191.      */
  192.     protected $addName = true;
  193.     
  194.     /**
  195.      * What is the parent to this element?
  196.      * 
  197.      * This points to some subclass of SForm_Container. It is set in
  198.      * SForm_Container::addElement().
  199.      * 
  200.      * @var        object 
  201.      */
  202.     protected $parent = null;
  203.     
  204.     /**
  205.      * Has the value for this element been set?
  206.      * 
  207.      * @var        boolean 
  208.      */
  209.     protected $valueSet = false;
  210.     
  211.     /**
  212.      * The local ID for this element
  213.      * 
  214.      * @var        string 
  215.      */
  216.     private $localId;
  217.     
  218.     /**
  219.      * The cached Global ID for this element
  220.      * 
  221.      * @var        string 
  222.      */
  223.     private $globalId;
  224.     
  225.     /**
  226.      * Is this element frozen?
  227.      * 
  228.      * @var        boolean 
  229.      */
  230.     private $frozen false;
  231.     
  232.     /**
  233.      * The label for this element
  234.      * 
  235.      * @var        string 
  236.      */
  237.     private $label;
  238.     
  239.     /**
  240.      * Rules to apply to this element
  241.      * 
  242.      * @var        array 
  243.      */
  244.     protected $rules = array();
  245.     
  246.     /**
  247.      * Are the rules & filters prevented from being modified?
  248.      * 
  249.      * When the rules are rendered (or otherwise used), they are locked to
  250.      * prevent someone from adding an additonal rule. This would invalidate any
  251.      * calculations done using the previous ruleset.
  252.      * 
  253.      * @var        boolean 
  254.      */
  255.     protected $locked = false;
  256.     
  257.     /**
  258.      * Is this a required element?
  259.      * 
  260.      * If an element is marked as required, the 'required' rule is added at
  261.      * render time and modifications can be made to the label HTML which mark
  262.      * this element as required.
  263.      * 
  264.      * @var        boolean 
  265.      */
  266.     protected $required = false;
  267.     
  268.     /**
  269.      * Has this element been validated?
  270.      * 
  271.      * Validated elements have a finished $errors array and will not be
  272.      * validated again.
  273.      * 
  274.      * @var        boolean 
  275.      */
  276.     protected $validated = false;
  277.     
  278.     /**
  279.      * A list of callback
  280.      */
  281.     
  282.     /**
  283.      * What were the results of the validation?
  284.      * 
  285.      * All error strings returned by validation are saved here. If there is no
  286.      * error string, then there was no error.
  287.      * 
  288.      * @var        array 
  289.      */
  290.     protected $errors = array();
  291.     
  292.     /**
  293.      * A list of filter callbacks
  294.      * 
  295.      * @var        array 
  296.      * @see        filter()
  297.      */
  298.     protected $filters = array();
  299.     
  300.     /**
  301.      * What access keys have been used already?
  302.      * 
  303.      * The default ones are in use by various systems.
  304.      * 
  305.      * @var        array 
  306.      * @todo        Comment in which default keys come from which sources.
  307.      */
  308.     private static $accesskeys array(
  309.                         'f' => true,
  310.                         'e' => true,
  311.                         'v' => true,
  312.                         'g' => true,
  313.                         'a' => true,
  314.                         't' => true,
  315.                         'h' => true,
  316.                         'd' => true,
  317.                         's' => true,
  318.                         'b' => true,
  319.                         'w' => true );
  320.  
  321.     /**
  322.      * Number the elements if not given an ID
  323.      * 
  324.      * @var        integer 
  325.      */
  326.     static protected $defId = 0;
  327.     
  328.     /**
  329.      * Attributes for this tag
  330.      * 
  331.      * @var        array 
  332.      */
  333.     protected $attributes = array();
  334.     
  335.     /**
  336.      * Global settings
  337.      * 
  338.      * Charset ('charset') is the the type of chars used to encode the HTML.
  339.      * 
  340.      * Auto access key ('autoAccessKey') controls whether access keys are
  341.      * automatically assigned to labels.
  342.      * 
  343.      * The element error color ('elmErrorColor') is the background color used on
  344.      * elements which threw some sort of error.
  345.      * 
  346.      * Xml close ('xmlClose') alters how the HTML tags are drawn. Mainly, do
  347.      * empty tags end in ' />' (XML, XHTML) or '>' (HTML).
  348.      * 
  349.      * Track id ('trackId') is the Local ID of the hidden element used to track
  350.      * this form.
  351.      * 
  352.      * The separator ('separator') is used in the Global ID to create sub-groups
  353.      * of elements.
  354.      * 
  355.      * The new submit value ('newSubmitValue') is what the JavaScript writes
  356.      * into the values of submit buttons when the form is successfully
  357.      * submitted.
  358.      * 
  359.      * The maximum upload file size ('maxFileSize') is the maximum upload file
  360.      * size. It defaults to 2 MB, which is the PHP 'upload_max_filesize' default.
  361.      * 
  362.      * @var        array 
  363.      * @see        self::getLabelHtml()
  364.      * @see        SForm_Renderer::rendJavaScript()
  365.      * @access    public
  366.      */
  367.     private static $options array(
  368.         'charset' => 'UTF-8',
  369.         'autoAccessKey' => 'ak',
  370.         'elmErrorColor' => 'yellow',
  371.         'xmlClose' => true,
  372.         'trackId' => '_trk',
  373.         'separator' => '-'// Can be used in CSS selectors
  374.                 'newSubmitValue' => ' Loading... ',
  375.         'maxFileSize' => '2097152',
  376.         'requiredSym' => '<span style="color:red;">*</span>');
  377.     
  378.     /**
  379.      * The rules
  380.      * 
  381.      * @var        array 
  382.      */
  383.     private static $regRules array(
  384.         'boolean' => array('SForm/Rule/Boolean.php''SForm_Rule_Boolean'),
  385.         'maxlength' => array(false'SForm_Rule_Maxlenth'),
  386.         'subset' => array(false'SForm_Rule_Subset'),
  387.         'required' => array(false'SForm_Rule_Required') );
  388.     
  389.     
  390.     /**
  391.      * Construct with an identity. Optionally add a label and more attributes.
  392.      * 
  393.      * $id is the Local ID of this element, and is similar to its name. If none
  394.      * is supplied, one will be created. The Local ID is different than the 'id'
  395.      * attribute that appears in the rendered form. The id attribute is referred
  396.      * to as the Global ID, and is the concatenation of the parent's Global ID,
  397.      * a colon, and this Local ID. The ID must contain only chars acceptable in
  398.      * the ID SGML token for the resulting page to be valid.
  399.      * 
  400.      * The $label is the the short string that describes this element. If it is
  401.      * rendered as HTML, it will automatically be enclosed in '<label>' tags
  402.      * which referr to the Global ID of this element. If enabled, an appropriate
  403.      * access key will also be found for this element. The access key will be
  404.      * rendered with '<span class="ak">' and '</span>' tags surrounding it. If
  405.      * you wish to communicate with your users that this access key may be used
  406.      * to access this field, I suggest that you use CSS like: '.ak{text-
  407.      * decoration:underline;}'.
  408.      * 
  409.      * Any additonal $attributes may be added and will be displayed when the
  410.      * element is rendered as HTML.
  411.      * 
  412.      * @param    string    Local ID of this element
  413.      * @param    string    Textual label for this element
  414.      * @param    array    Array of extra attributes
  415.      * @link        http://www.w3.org/TR/html4/types.html#h-6.2
  416.      * @see        getLocalId()
  417.      * @see        getGlobalId()
  418.      * @see        getLabel()
  419.      * @see        getLabelHtml()
  420.      */
  421.     public function __construct($id null$label null$attributes null){
  422.         
  423.         /*
  424.          * We should try to dynamically make this as needed from what the parent
  425.          * recommends (if avl). Also check the $attributes array for 'name' and
  426.          * 'id'.
  427.          */
  428.         if($id === null){
  429.             $id self::$defId++;
  430.         }
  431.         
  432.         $this->localId = (string) $id;
  433.         $this->label = (string) $label;
  434.         
  435.         //Apply each attribute
  436.         if(!empty($attributes)){
  437.             if(!is_array($attributes)){
  438.                 throw new Exception('Please form attributes in array("attribute" ' .
  439.                         '=> "value", ...) form. It\'s ' .
  440.                         'much faster than parsing a string.');
  441.             }
  442.             
  443.             foreach($attributes as $name => $value){
  444.                 $this->setAttribute($name$value);
  445.             }
  446.         }
  447.     }
  448.     
  449.     /**
  450.      * This is the page-wide unique ID
  451.      * 
  452.      * The Global ID is a page-unique id which is a concatination of the
  453.      * parent's Global ID, a colon, and the Local ID. This is done so that
  454.      * element groups can be nested arbitrarily deeply with non-uniqe IDs (ex:
  455.      * an array: '0','1','2'...).
  456.      * 
  457.      * The Global ID is used as the 'id' HTML attribute and is typically also
  458.      * used as the 'name' attribute.
  459.      * 
  460.      * The Global ID cannot be determined until the element is "rooted" to a
  461.      * SForm object.
  462.      * 
  463.      * @return    string    A globally usable ID
  464.      */
  465.     public function getGlobalId(){
  466.         if($this->globalId){
  467.             return $this->globalId;
  468.         }
  469.         
  470.         if(empty($this->parent)){
  471.             throw new Exception('An element must have a parent, please assign me to one. I\'m lonely.');
  472.         }
  473.         
  474.         return $this->globalId $this->parent->getGlobalId().
  475.                                 self::getOption('separator').
  476.                                 $this->getLocalId();
  477.     }
  478.     
  479.     /**
  480.      * A Locally usable ID
  481.      * 
  482.      * The local ID is the name that a parent uses to identify its child
  483.      * element.
  484.      * 
  485.      * @return    string    A locally usable ID
  486.      */
  487.     public function getLocalId(){
  488.         return $this->localId;
  489.     }
  490.     
  491.     /**
  492.      * The value of this element
  493.      * 
  494.      * The value returned is the same as if the form was rendered, displayed,
  495.      * then submitted by the user, in its current state.
  496.      * 
  497.      * @return    mixed    Element's current value
  498.      * @see        setValue()
  499.      * @see        setDefault()
  500.      * @uses        $valueSet    Is the current value overridden by setValue?
  501.      */
  502.     public function getValue(){
  503.         
  504.         //Return the set value
  505.         if($this->valueSet){
  506.             return $this->filterValue($this->getAttribute('value'));
  507.         }
  508.  
  509.         //Return the value of whatever was submited
  510.         $val $this->getRequest($this->getGlobalId());
  511.         if($val !== null){
  512.             $this->setValue($val);
  513.             return $this->filterValue($val);
  514.         }
  515.         
  516.         //Return what we have
  517.         return $this->filterValue($this->getAttribute('value'));
  518.     }
  519.     
  520.     /**
  521.      * Filters the value
  522.      * 
  523.      * Filters a value that is about to be returned from {@link getValue()}.
  524.      * 
  525.      * @var        string    Raw value
  526.      * @return    string    Filtered value
  527.      * @see        getValue()
  528.      */
  529.     protected function filterValue($val){
  530.         foreach($this->filters as $fil){
  531.             $func $fil[0];
  532.             $fil[0$val;
  533.             $val call_user_func_array($func$fil);
  534.         }
  535.         return $val;
  536.     }
  537.     
  538.     /**
  539.      * Set the value of this element
  540.      * 
  541.      * This sets the current value of the element, overriding any current value.
  542.      * 
  543.      * @param    string    Element's value.
  544.      * @see        setDefault()
  545.      * @uses        $valueSet    Overrides any current value
  546.      */
  547.     public function setValue($value){
  548.         $this->valueSet = true;
  549.         $this->setAttribute('value'$value);
  550.     }
  551.     
  552.     /**
  553.      * Set the default value of this element
  554.      * 
  555.      * This sets the default value of this element. If there is a user-submitted
  556.      * value or a {@link setValue()}, it overrides this one.
  557.      * 
  558.      * @param    string    Element's value.
  559.      * @see        setValue()
  560.      */
  561.     public function setDefault($value){
  562.         if(!$this->valueSet){
  563.             $this->setAttribute('value'$value);
  564.         }
  565.     }
  566.  
  567.     /**
  568.      * Markup a label with an accesskey
  569.      * 
  570.      * Marks up the first char in a label with CSS that can show the user which
  571.      * key is the access key for this form. If none of the chars match the
  572.      * requested one, the char is appended to the end of the label in
  573.      * parenthesis.
  574.      * 
  575.      * The class of the access key is set with the autoAccessKey option.
  576.      * 
  577.      * @param    string    Label to be marked up
  578.      * @param    string    Selected char
  579.      * @return    string    Marked up string
  580.      */
  581.     private function markupLabelAK($lbl$ak){
  582.         
  583.         $cls self::getOption('autoAccessKey');
  584.         
  585.         if(($i stripos($lbl$ak)) === false){
  586.             return self::escHtml($lbl).' (<span class="'.$cls.'">'.$ak.'</span>)';
  587.         else {
  588.             
  589.             /*
  590.              * We need to markup the access key, but escape HTML at the same
  591.              * time. It's more tricky than it sounds because we don't wan't to
  592.              * escape any chars in the HTML tags.
  593.              */
  594.             $offset strlen(self::escHtml(substr($lbl0$i)));
  595.             $replen strlen(self::escHtml(substr($lbl$i1)));
  596.             
  597.             $esc self::escHtml($lbl);
  598.             
  599.             return substr_replace($esc'<span class="'.$cls.'">'.$lbl[$i].'</span>'$offset$replen);
  600.         }
  601.     }
  602.     
  603.     /**
  604.      * Generates an access key and updated label
  605.      * 
  606.      * Uses an access key to generate a marked up label.
  607.      * 
  608.      * If access key is simply true, it generates an access key by searching
  609.      * through the chars of the label for the first one that is a regular char
  610.      * ([a-z0- 9]) and is not already in use. If none are found, numbers
  611.      * starting from the left side of the std keyboard are used.
  612.      * 
  613.      * @param    string    Label
  614.      * @param    string    Requested access key
  615.      * @return    mixed    Array of updated label if possible, null otherwise
  616.      * @uses        markupLabelAK()    Markup the label with the found key
  617.      * @uses        $accesskeys    Determine which keys are used
  618.      */
  619.     protected function autoAK($lbl$ak){
  620.         //Use requested access key
  621.         if($ak !== true){
  622.             return array($ak$this->markupLabelAK($lbl$ak));
  623.         }
  624.         
  625.         $lblLower strtolower($lbl);
  626.         $len strlen($lbl);
  627.  
  628.         //Look for an access key in the label
  629.         for($i 0$i $len$i++){
  630.             $chr $lblLower[$i];
  631.             //Only regular ASCII chars
  632.             if(ctype_alnum($chr&& !i