Source for file SForm.php
Documentation is available at SForm.php
* A fast & powerful PEAR_QuickForm replacement
* SForm is short for Seth's Form. Why? Because it's easy to remember, and even
* easier to type. SForm was started out of frusteration with PEAR's QuickForm.
* It was too slow, it wouldn't allow me to create complex rules, the code was
* antiquidated, it didn't nest groups, the API was convoluted, the list went
* on. After spending a few days wrestling with QF, I came to the realization
* that I could code my own package in the time it took me to figure out how to
* bend QuickForm to my will. So that's what I did.
* SForm automatically does the standard QF things, like {@link }
* SForm_Rule::toJavaScript() JavaScript generation}, rendering plugins, both
* server-side and {@link SForm_Element::validate() client side validation},
* and is very customizable. Some things not included in QF include non-
* enforced <b>XHTML 1.1 compliance</b>, <b>{@link SForm_Elm_Select::lock()}
* intrinsic rules}</b>, <b>nested groups</b>, and <b>{@link }
* http://www.webstandards.org/learn/tutorials/accessible-forms/beginner/
* automatic accessibility features}</b> like
* <label> tags, optgroups, JavaScript highlighting of errors, and access keys.
* I've tried to keep the API very similar to QuickForm, but I've removed things
* that I found unnecessary or redundant.
* SForm is also <i>fast</i>.
* The small form that I originally used to develop SForm had a render time of
* 0.10-0.28 seconds when no form was created. Using QuickForm to build a form
* on the page increased the render time to 0.33-0.68 secs. Replacing QuickForm
* with SForm dropped the render time to 0.18-0.42 secs. SForm is almost
* <b>three times faster</b> than QuickForm in this informal test. (All trials
* used eAccelerator with caching and optimization enabled.)
* The SForm API is intentionally very similar to QuickForm. This example was
* converted directly from the {@link http://pear.php.net/manual/en/package.}
* html.html-quickform.tutorial.php QuickForm documentation}. All of the
* examples on {@link http://www.midnighthax.com/quickform.php Keith Edmunds's}
* QuickForm tutorial} also work with similar modifications. It should be noted
* that to make this example XHTML 1.1 compliant, the elements would need to be
* // Load the main classes
* require_once 'SForm.php';
* // Instantiate the SForm object
* $form = new SForm('firstForm');
* // Set defaults for the form elements
* $form->setDefaults(array( 'name' => 'Joe User' ));
* // Add some elements to the form
* $form->addElement('header', null, 'SForm tutorial example');
* $form->addElement('text', 'name', 'Enter your name:',
* array('size' => 50, 'maxlength' => 255));
* $form->addElement('submit', null, 'Send');
* // Define filters and validation rules
* $form->applyFilter('name', 'trim');
* $form->addRuleDep('name', 'Please enter your name', 'required', null,'client');
* // Try to validate a form
* if ($form->validate()) {
* echo '<h1>Hello, ' . htmlspecialchars($form->exportValue('name')) . '!</h1>';
* Note that in the example, addRule() has been changed to {@link }
* SForm::addRuleDep() addRuleDep()}, where 'Dep' stands for
* depriciated. For more information, see the {@link SForm_Rule}
* SForm_Rule documentation}.
* <b>Putting it all together</b>:
* {@example SForm_usage.php}
* Look in SForm_usage.php for this source
* <b>Important differences with the QuickForm API include</b>:
* - The construtor. The $target attribute {@link SForm::__construct() has been removed} because it breaks compliance with XHTML 1.1.
* - Rule creation and usage. SForm uses a much more object oriented {@link SForm_Rule Rule model}.
* SForm requires at least PHP version 5, but version 5.1 is recommended.
* Copyright (c) 2006, Seth Price <{@link mailto:seth@pricepages.org seth@pricepages.org}> All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - 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.
* - The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* @copyright Copyright (c) 2006, Seth Price
* @author Seth Price <seth@pricepages.org>
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @see SForm::__construct()
* ./phpdoc -t ../docs -f ../scripts/SForm.php -d ../scripts/SForm -ti "SForm" -o HTML:frames:phpdoc.de
* FIXME - clean up exception messages and values.
define('FORM_ERR_PARENT_EXISTS', 1);
* Create the count interface if needed
* The Countable interface is not enabled by default in PHP 5.0, so we'll
* A element is a "leaf" that is actually rendered
* All elements and containers are decendents of this class. That said, this
* class is intended to cover as much common functionality as possible.
* This is the element's tag ('input', 'select', 'form', etc) and should be
* set by the children as needed.
protected $tag = 'input';
* Is this element's tag empty?
* A tag is empty if it can't contain elements. Examples of empty tags
* include '<input />' and '<img />'. Non-empty tags include '<form>' and
* Should we add the 'name' attribute?
* What is the parent to this element?
* This points to some subclass of SForm_Container. It is set in
* SForm_Container::addElement().
* Has the value for this element been set?
* The local ID for this element
* The cached Global ID for this element
* Is this element frozen?
* The label for this element
* Rules to apply to this element
* Are the rules & filters prevented from being modified?
* When the rules are rendered (or otherwise used), they are locked to
* prevent someone from adding an additonal rule. This would invalidate any
* calculations done using the previous ruleset.
* Is this a required element?
* If an element is marked as required, the 'required' rule is added at
* render time and modifications can be made to the label HTML which mark
* this element as required.
* Has this element been validated?
* Validated elements have a finished $errors array and will not be
* What were the results of the validation?
* All error strings returned by validation are saved here. If there is no
* error string, then there was no error.
* A list of filter callbacks
* What access keys have been used already?
* The default ones are in use by various systems.
* @todo Comment in which default keys come from which sources.
private static $accesskeys = array(
* Number the elements if not given an ID
* Attributes for this tag
* Charset ('charset') is the the type of chars used to encode the HTML.
* Auto access key ('autoAccessKey') controls whether access keys are
* automatically assigned to labels.
* The element error color ('elmErrorColor') is the background color used on
* elements which threw some sort of error.
* Xml close ('xmlClose') alters how the HTML tags are drawn. Mainly, do
* empty tags end in ' />' (XML, XHTML) or '>' (HTML).
* Track id ('trackId') is the Local ID of the hidden element used to track
* The separator ('separator') is used in the Global ID to create sub-groups
* The new submit value ('newSubmitValue') is what the JavaScript writes
* into the values of submit buttons when the form is successfully
* The maximum upload file size ('maxFileSize') is the maximum upload file
* size. It defaults to 2 MB, which is the PHP 'upload_max_filesize' default.
* @see self::getLabelHtml()
* @see SForm_Renderer::rendJavaScript()
private static $options = array(
'elmErrorColor' => 'yellow',
'separator' => '-', // Can be used in CSS selectors
'newSubmitValue' => ' Loading... ',
'maxFileSize' => '2097152',
'requiredSym' => '<span style="color:red;">*</span>');
private static $regRules = array(
'boolean' => array('SForm/Rule/Boolean.php', 'SForm_Rule_Boolean'),
'maxlength' => array(false, 'SForm_Rule_Maxlenth'),
'subset' => array(false, 'SForm_Rule_Subset'),
'required' => array(false, 'SForm_Rule_Required') );
* Construct with an identity. Optionally add a label and more attributes.
* $id is the Local ID of this element, and is similar to its name. If none
* is supplied, one will be created. The Local ID is different than the 'id'
* attribute that appears in the rendered form. The id attribute is referred
* to as the Global ID, and is the concatenation of the parent's Global ID,
* a colon, and this Local ID. The ID must contain only chars acceptable in
* the ID SGML token for the resulting page to be valid.
* The $label is the the short string that describes this element. If it is
* rendered as HTML, it will automatically be enclosed in '<label>' tags
* which referr to the Global ID of this element. If enabled, an appropriate
* access key will also be found for this element. The access key will be
* rendered with '<span class="ak">' and '</span>' tags surrounding it. If
* you wish to communicate with your users that this access key may be used
* to access this field, I suggest that you use CSS like: '.ak{text-
* decoration:underline;}'.
* Any additonal $attributes may be added and will be displayed when the
* element is rendered as HTML.
* @param string Local ID of this element
* @param string Textual label for this element
* @param array Array of extra attributes
* @link http://www.w3.org/TR/html4/types.html#h-6.2
public function __construct($id = null, $label = null, $attributes = null){
* We should try to dynamically make this as needed from what the parent
* recommends (if avl). Also check the $attributes array for 'name' and
$this->localId = (string) $id;
$this->label = (string) $label;
if(!is_array($attributes)){
throw new Exception('Please form attributes in array("attribute" ' .
'=> "value", ...) form. It\'s ' .
'much faster than parsing a string.');
foreach($attributes as $name => $value){
* This is the page-wide unique ID
* The Global ID is a page-unique id which is a concatination of the
* parent's Global ID, a colon, and the Local ID. This is done so that
* element groups can be nested arbitrarily deeply with non-uniqe IDs (ex:
* an array: '0','1','2'...).
* The Global ID is used as the 'id' HTML attribute and is typically also
* used as the 'name' attribute.
* The Global ID cannot be determined until the element is "rooted" to a
* @return string A globally usable ID
throw new Exception('An element must have a parent, please assign me to one. I\'m lonely.');
return $this->globalId = $this->parent->getGlobalId().
self::getOption('separator').
* The local ID is the name that a parent uses to identify its child
* @return string A locally usable ID
* The value of this element
* The value returned is the same as if the form was rendered, displayed,
* then submitted by the user, in its current state.
* @return mixed Element's current value
* @uses $valueSet Is the current value overridden by setValue?
//Return the value of whatever was submited
* Filters a value that is about to be returned from {@link getValue()}.
* @return string Filtered value
* Set the value of this element
* This sets the current value of the element, overriding any current value.
* @param string Element's value.
* @uses $valueSet Overrides any current value
* Set the default value of this element
* This sets the default value of this element. If there is a user-submitted
* value or a {@link setValue()}, it overrides this one.
* @param string Element's value.
* Markup a label with an accesskey
* Marks up the first char in a label with CSS that can show the user which
* key is the access key for this form. If none of the chars match the
* requested one, the char is appended to the end of the label in
* The class of the access key is set with the autoAccessKey option.
* @param string Label to be marked up
* @param string Selected char
* @return string Marked up string
private function markupLabelAK($lbl, $ak){
$cls = self::getOption('autoAccessKey');
if(($i = stripos($lbl, $ak)) === false){
return self::escHtml($lbl). ' (<span class="'. $cls. '">'. $ak. '</span>)';
* We need to markup the access key, but escape HTML at the same
* time. It's more tricky than it sounds because we don't wan't to
* escape any chars in the HTML tags.
$esc = self::escHtml($lbl);
return substr_replace($esc, '<span class="'. $cls. '">'. $lbl[$i]. '</span>', $offset, $replen);
* Generates an access key and updated label
* Uses an access key to generate a marked up label.
* If access key is simply true, it generates an access key by searching
* through the chars of the label for the first one that is a regular char
* ([a-z0- 9]) and is not already in use. If none are found, numbers
* starting from the left side of the std keyboard are used.
* @param string Requested access key
* @return mixed Array of updated label if possible, null otherwise
* @uses markupLabelAK() Markup the label with the found key
* @uses $accesskeys Determine which keys are used
protected function autoAK($lbl, $ak){
//Use requested access key
return array($ak, $this->markupLabelAK($lbl, $ak));
//Look for an access key in the label
for($i = 0; $i < $len; $i++ ){
//Only regular ASCII chars
if(ctype_alnum($chr) && !isset (self::$accesskeys[$chr])){
self::$accesskeys[$chr] = true;
return array($chr, $this->markupLabelAK($lbl, $chr));
//Look for an access key in numbers
for($i = 1; $i < 11; $i++ ){
if(empty(self::$accesskeys[$ak])){
self::$accesskeys[$ak] = true;
return array($ak, $this->markupLabelAK($lbl, $ak));
* Return the label with markup added
* Generates the marked up 'label' tag. This includes the 'for' attribute,
* which links this label to the appropriate form element. The label allows
* software (and the people who use it) to understand the layout of your
* form without being able to view it. If the element is required, the value
* of option 'requiredSym' is appended to the beginning of the label.
* If requested, an access key is generated, and added to the tag. The
* access key links this label to a specific element. Not only does an
* access key make the page more friendly for people with motor
* disabilities, it will now be easier to quickly navigate the fields in
* your form for data entry.
* Access keys can be enabled, set, or disabled via the passed parameters.
* By default, $ak is true and an access key is generated. If a char is
* passed, that char is used as the access key and the label is marked up
* approriately. If false is passed, no access key is used.
* @param mixed Do we automatically generate an access key?
* @link http://www.webstandards.org/learn/tutorials/accessible-forms/beginner/
* @link http://alistapart.com/articles/accesskeys/
//Nothing special when frozen (or non-existant)
//If it has these attributes, we should be able to do some work transparently
if(self::getOption('autoAccessKey') && $accesskey && !$this->getAttribute('disabled')){
$ak = $this->autoAK($lbl, $accesskey);
//Add the required symbol
$req = self::getOption('requiredSym');
//Create and save the label
return $req. '<label for="'. $id. '">'. self::escHtml($lbl). '</label>';
return $req. '<label for="'. $id. '" accesskey="'. $ak[0]. '">'. $ak[1]. '</label>';
* Return the label string (no markup)
* @return string The raw label
* Locks & adds any last minute stuff
* This is here to be overridden as needed by subclasses to add any rules,
* filters, and values immidiately before they are locked. Remember to check
* if it's already locked first, because an element may be locked more than
protected function lock(){
* Retrieve any JavaScript that should be run onsubmit
* Locks the rules and iterates through them to produce a JavaScript string
* appropriate to be run on submission of the form.
* @return string Rendered JavaScript
* @uses SForm_Rule::toJavaScript()
foreach($this->rules as $rule){
$js .= $rule->toJavaScript();
* Create a HTML representation of this element
* Creates a HTML tag appropriate to input a value for this element. At
* minimum, 'id' and 'name' attributes are included. Additonal attributes
* can be added via the methods appropriate for the element. Custom
* attributes can be added via setAttribute().
* If this element is frozen, the string representation is returned instead
//Some elements can't have a name attribute
if(self::getOption('xmlClose')){
return '<'. $this->tag. self::getAttributesString($this->attributes). $slash. '>';
return '<'. $this->tag. self::getAttributesString($this->attributes). '>';
* Close an open HTML tags
* @return string The HTML needed to close this element
return '</'. $this->tag. '>';
* Do we have any hidden tags to append?
* Append hidden tags seperately from regular HTML tags so they can be
* collected at the end of the form. That way the visible part of the form
* is parsed and displayed to the user first.
* By default, hidden elements are only generated when this element is
* @return string HTML of the hidden element
if(self::getOption('xmlClose')){
return '<input type="hidden"'. self::getAttributesString($this->attributes). $slash. '>';
* How does this value look as a HTML string?
return self::escHtml($this->getValue());
* All available options are pre-set in the {@link $options} array, so we
* will be avoiding some frusteration if we throw an error on a bad option
* selection. Otherwise, we could think that we are retrieving a null value,
* but we have just misspelled the index.
* @param string Option name
* @param string Option value
final public static function setOption($name, $value){
if(isset (self::$options[$name])){
self::$options[$name] = $value;
throw new Exception('The requested option "'. $name. '" doesn\'t exist.');
* All available options are pre-set in the {@link $options} array, so we
* will be avoiding some frusteration if we throw an error on a bad option
* selection. Otherwise, we could think that we are retrieving a null value,
* but we have just misspelled the index.
* @param string Option name
* @return string Option value
final public static function getOption($name){
if(isset (self::$options[$name])){
return self::$options[$name];
throw new Exception('The requested option "'. $name. '" doesn\'t exist.');
* Set an attribute in $this
* @param string Attribute to set
* @param string Value to set (sets value = attribute if null)
* @param string Attribute to unset
* Get an attribute in $this
* @param string Attribute to get
* @return string Attribute's value
* Creates an HTML attribute string from an array
* @param array Attributes as: attribute => value
* @return string HTML string
foreach($attributes as $attr => $value){
$str .= ' '. $attr. '="'. self::escHtml($value). '"';
* Escape HTML in a consistant manner
* @param string Text to escape
* @return string Text HTML escaped
public static function escHtml($str){
self::getOption('charset'));
* Append this leaf element to the render queue
* The renderer is passed to this element so it can call whichever method is
* wanted. (ie: toHtml(), toJavaScript(), and/or toString()).
* @param object The renderer to use
* @uses SForm_Renderer::append()
protected function render(SForm_Renderer $rend){
* Add a rule to this element
* A rule assigned to this element will be controled by this element. All
* validation will come through this element, and any errors will be
* returned via this element. All JavaScript will also be transmitted via
* If the first argument is a rule object, it will simply be added to this
* element. If it is a string, it is assumed that you want to {@link }
* createRule() create a rule} which is linked to this element.
* Each rule can only be assigned to one element, but that element does not
* need to be used by the rule. For example: an element might validate two
* elements in a group, but if the element is added to the group, then any
* errors will be displayed at the group level.
* @param mixed The rule to add
* @return object The added rule
* @uses SForm_Rule::assignParent()
public function addRule($rule, $message = null, $arg2 = null, $arg3 = null,
$arg4 = null, $arg5 = null, $arg6 = null){
//Create the rule if needed
$rule = $this->createRule($rule, $message, $arg2, $arg3, $arg4, $arg5, $arg6);
throw new Exception('The passed parameter was neither a rule ' .
'object or the string of a type.');
//Can't add rules to locked elements
* The results from the rule set have been calculated. If we add any
* more rules, it will make the previous result invalid (and maybe
throw new Exception('Attempting to add a rule when they\'ve already ' .
'been used. You\'re doing this in the wrong order.');
* We should be able to avoid adding duplicate rules to an element,
* I'll add exceptions as I find them.
if(isset ($this->rules[$class])){
trigger_error('A rule of type "'. $class. '" has already been added to this element "'. $this->getLocalId(). '".', E_USER_NOTICE);
$this->rules[$class] = $rule;
$rule->assignParent($this);
* Creates & returns a rule
* @param string Name of the rule
* @param string Error message for the rule
* @return object Created rule
protected function createRule($rule, $message = null, $arg2 = null, $arg3 = null,
$arg4 = null, $arg5 = null, $arg6 = null){
if(empty(self::$regRules[$rule])){
throw new Exception('The given rule type "'. $rule. '" is not built in. ' .
'If you are adding a custom rule, please simply add ' .
'the object via addRule(). You might be trying to use addRule() ' .
'with the QuickForm API. Please refer to the docs.');
$meta = self::$regRules[$rule];
if($meta[0] && !class_exists($meta[1])){
* This is an ugly way of creating an object with an "arbitrary"
* number of arguments. If you know of a better way, please contact
$rule = new $meta[1]($message, $this, $arg2, $arg3, $arg4, $arg5, $arg6);
throw new Exception('Rule not a subclass of SForm_Rule.');
* Apply a filter to the value
* This is passed a callback function which applies some sort of filter to
* any value returned from {@link getValue()}. Any additonal arguments to
* this function will be appended to the arguments of the callback function.
* This cannot be called after {@link validate()} or {@link render()},
* because the element is locked.
* @param mixed Callback function
public function filter($callback){
throw new Exception('Attempted to add a filter to a locked element.');
* Did this element's validation generate any errors? If it did, return
* those errors as an HTML string. Each error is seperated by a '<br />'
* @return string Any errors that were produced
if(self::getOption('xmlClose')){
* Is this a valid element?
* Performs server side validation using any of the added rules. Calls
* {@link lock()} to provide a chance to load any intrinsic rules.
* @return boolean Returns true if no errors are found.
* @uses SForm_Rule::toHtml()
foreach($this->rules as $rule){
$this->errors[] = $rule->toString();
* Set if this element is frozen
* @param boolean Is it frozen?
public function freeze($bool = true){
* Is this element marked as frozen?
* @return boolean Is this element frozen?
* Is this element required?
* @return boolean True if this element is required
* Assign this element's parent
* @param object Parent to assign
* @see SForm_Container::addElement()
throw new Exception('Parent already exists', FORM_ERR_PARENT_EXISTS);
* Returns a value from the request
* @param string Value to retrieve
* @return mixed Value or null
return $this->parent->getRequest($val);
throw new Exception('Unable to get a request value until I have a ' .
* Was this form submitted?
* @return boolean Has it been submitted?
return $this->parent->wasSubmitted();
throw new Exception('I need to be grounded in a parent.');
* A container holds one or more elements
* A container serves as a parent for as many elements (and other containers) as
* needed. It also inherits all of the properties of SForm_Element, so you can
* assign rules to containers, they can produce JavaScript, they have labels,
* and have opening and closing HTML tags.
* Containers are never empty tags
* Children of this container
private $children = array();
* Filters which will be applied to the children of this container
* Rules which will be applied to the children of this container
* List of registered elements
* 'name' => array('include str', 'class name')
private static $regElms = array(
'fieldset' => array(false, 'SForm_Fieldset'),
'header' => array('SForm/Elm/Header.php', 'SForm_Elm_Header'),
'hidden' => array(false, 'SForm_Elm_Hidden'),
'checkbox' => array(false, 'SForm_Elm_Checkbox'),
'text' => array(false, 'SForm_Elm_Text'),
'submit' => array(false, 'SForm_Elm_Submit'),
'reset' => array(false, 'SForm_Elm_Reset'),
'textarea' => array(false, 'SForm_Elm_Textarea'),
'select' => array(false, 'SForm_Elm_Select'),
'group' => array(false, 'SForm_Group'));
* Does the current element have children?
* Implemented as part of the {@link http://www.php.net/~helly/php/ext/spl/interfaceRecursiveIterator.html RecursiveIterator}
* @return boolean Is the current element a container?
* Return the current element
* Implemented as part of the {@link http://www.php.net/~helly/php/ext/spl/interfaceRecursiveIterator.html RecursiveIterator}
* @return object The current child
* Move the current element to the first one
* Implemented as part of the {@link http://www.php.net/manual/en/language.oop5.iterations.php}
* Iterator SPL interface}
final public function rewind(){
* Return the current element
* Implemented as part of the {@link http://www.php.net/manual/en/language.oop5.iterations.php}
* Iterator SPL interface}
* Return the key of the current element
* Implemented as part of the {@link http://www.php.net/manual/en/language.oop5.iterations.php}
* Iterator SPL interface}
* @return string Returns the current Local ID
final public function key(){
return key($this->children);
* Advance pointer and return the new current
* Implemented as part of the {@link http://www.php.net/manual/en/language.oop5.iterations.php}
* Iterator SPL interface}
* @return object New current object
final public function next(){
return next($this->children);
* Is the current ponter valid?
* Implemented as part of the {@link http://www.php.net/manual/en/language.oop5.iterations.php}
* Iterator SPL interface}
* @return boolean True if the current pointer is valid
final public function valid(){
return (boolean) current($this->children);
* Return number of children
* Implemented as part of the {@link http://www.php.net/~helly/php/ext/spl/interfaceCountable.html Countable SPL interface}
* @return integer How many children are there
final public function count(){
return count($this->children);
* Adds an element to this container
* This function has behavior that changes depending on the first argument
* If the first argument is an element object, it is added to this container
* If the first argument is a string, the arguments are first passed through
* {@link createElement()} and an element is created. That element is then
* added to this container as a child.
* The element is then returned.
* @param mixed Element or type of element to add
* @return object Added element
public function addElement($elm, $arg1 = null, $arg2 = null, $arg3 = null,
$arg4 = null, $arg5 = null, $arg6 = null){
$elm = $this->createElement($elm, $arg1, $arg2, $arg3, $arg4, $arg5, $arg6);
throw new Exception('The passed parameter was neither an element ' .
'object or the string of a type.');
$elmId = $elm->getLocalId();
if(isset ($this->children[$elmId])){
throw new Exception('Child by this ID already exists');
$elm->assignParent($this);
$this->children[$elmId] = $elm;
* Creates whichever argument is specified by the first argument.
* Valid strings are generally the same as the the element that you are
* trying to create. A '<select>' element is created with a 'select' string.
* Parameters past the first will be passed to the constructor of the
* @param string Type of element to add.
* @param mixed First argument for object constructor...
* @return object Created object.
public static function createElement($elm, $arg1 = null, $arg2 = null, $arg3 = null,
$arg4 = null, $arg5 = null, $arg6 = null){
if(empty(self::$regElms[$elm])){
throw new Exception('The given element type "'. $elm. '" is not built in. ' .
'If you have created a custom element, please simply ' .
'pass the object to addElement().');
$meta = self::$regElms[$elm];
if($meta[0] && !class_exists($meta[1])){
* This is an ugly way of creating an object with an "arbitrary"
* number of arguments. If you know of a better way, please contact
$elm = new $meta[1]($arg1, $arg2, $arg3, $arg4, $arg5, $arg6);
throw new Exception('Element not a subclass of SForm_Element.');
* Returns an element using the Global ID
* The Global ID is the ID used in the HTML document or by getElementById()
* @param string Global ID to fetch
* @return object Requested object (or null)
throw new Exception('Unable to retrieve an element by Global ID when ' .
'this container isn\'t rooted to a form. Please add me to ' .
$this->parent->getGlobalElement($id);
* Returns an element using the Local ID
* The Local ID is the ID that passed to an element's constructor. It is
* useful for reequesting a specific child of an element. For iterating
* through elements, see the Iterator interface. Specifically {@link }
* @param string Local ID to fetch
* @return object Requested obect (or null)
if(empty($this->children[$id])){
return $this->children[$id];
* Removes an element from this group
* @param string The id of the object to remove
* @return object The removed object
unset ($this->children[$id]);
* Apply a filter to the elements
* This is the same as using {@link SForm_Element::filter()}, but the first
* argument is the child to apply this filter to.
* The filter is not applied to child elements until this container is
* {@link render() rendered} or {@link validate() validated}. If the child
* doesn't exist at that time, an exception is thrown.
* If you wish to apply the filter to <i>this</i> container's value, see
* @param string Element to apply the filter too
* @param mixed The filter callback
* @see SForm_Element::filter()
* Create a HTML representation of this container
* Constructs the HTML tag that opens this container. Normally this is
* either a <form> tag or a <fieldset> tag. The 'id' attribute is
* automatically filled with this container's Global ID. Other attributes
* may be added via {@link setAttribute()}.
* @return string Opening tag for this container
return '<'. $this->tag. self::getAttributesString($this->attributes). '>';
* Add any requested filters and rules, then lock this container
* We override the parent in order to add filters and rules.
protected function lock(){
foreach($this->children as $elm){
throw new Exception('The element "'. $elmId. '" wasn\'t ' .
'found when we were trying to apply a filter.');
* Validates this container and all of its children. Keep in mind that no
* rules can be added after an element is validated because that would
* invalidate the validation. If there was no request submitted, it is
* assumed that validation is not passed.
* Rules can be added via {@link addRule()}.
* @return boolean Is this container & its children valid?
* @uses parent::validate()
//No request? No validate.
foreach($this->children as $child){
* Set the frozen status of this container and its children
* Setting a container as frozen. Being frozen doesn't have an effect on
* containers. {@link parent::freeze() Frozen elements} will prevent their
* values from being altered, though.
* @param boolean Are we freezing this or thawing it?
public function freeze($bool = true){
foreach($this->children as $child){
* Render this container and all elements in it
* @param object The renderer to use
protected function render(SForm_Renderer $rend){
foreach($this->children as $child){
* I am the alpha and the omega, I am the form.
* The SForm is the root for all elements, and they will not fully funcion
* without being rooted to a SForm. All rendering operations should begin
* with {@link SForm::render()}, validation begins with {@link SForm::validate()},
* and processing can be accomplished with {@link SForm::toArray()}. For more
* information on SForm usage, see the {@link SForm.php package documentation}.
* Why yes, this is a form
* Defaults are stored here
* Are we tracking the submit?
* The ID parameter sets both the Global ID and the Local ID to be the same,
* since we are at the top level. All children of this form will use this ID
* as a prefix to their Global IDs.
* The method determines how we will send the data that is input. A 'get'
* method sends the data in the URL after a '?' and 'post' sends the data in
* the body of the HTTP request. An advantage of using 'get' is the
* results of the submission can be book marked. A disadvantage of using
* 'get' is that a form can be maliciously submitted without the user's
* knowledge by making them access the URL (ex: an embedded imag). 'get'
* requests are also awkward for large amounts of data.
* The action is what URL to send the data to. Generally you want to send
* the data back to the original URL so you can continue processing it and
* present errors if needed.
* Include extra attributes if you feel lucky.
* $trackSubmit inserts an extra hidden element. If this element isn't
* included in the submission, it is ignored. It defaults to true. Be
* careful when setting this to false, because the submission may then
* validate without a button ever being clicked. You might want to add a
* required rule to prevent this from happening.
* <b>Changes from HTML_QuickForm</b>:
* $target has been removed because it is not in XHTML 1.1. Use $attributes
* if you wish to add it back.
* $action is required in XHTML 1.1, so if one isn't provided, $_SERVER
* ['SCRIPT_URL'] is used.
* $trackSubmit has been set to true by default, because otherwise a form
* without required elements may validate without being submitted.
* $id does not apply a 'name' attribute to the form because this is illegal
* @param string This form's ID
* @param string Method attribute of the form ('post' or 'get')
* @param string Action attribute of the form
* @param array Custom attributes for this form
* @param boolean Track whether this form was submitted.
function __construct($id, $method = 'post', $action = '', $attributes = null, $trackSubmit = true){
parent::__construct($id, '', $attributes);
$this->request = & $_POST;
$this->request = & $_REQUEST;
throw new Exception('Method type not recognized: '. $method);
//Set the action (required in XHTML 1.1)
if(isset ($_SERVER['SCRIPT_URL'])){
$action = $_SERVER['SCRIPT_URL'];
// SCRIPT_URL is not set in PHP 5.1
elseif(isset ($_SERVER['SCRIPT_NAME'])){
$action = $_SERVER['SCRIPT_NAME'];
//Hopefully the browser will take care of it
//If we are tracking submit, then we need to have a hidden field
//Setup a required hidden element
* Get an element by its Global ID
* Retrieves an element based on it's Global ID. When at all possible, I'd
* suggest that you retrieve elements via {@link getElement()}.
* @param string Global ID
* @return object Requested element (or null)
return $this->recursiveGetElement(strval($id), $this);
* Find an element by its Global ID
* @param string Part of Global ID
* @param object Container we are currently searching in
* @return object The requested element
private function recursiveGetElement($id, SForm_Container $cont){
$sep = self::getOption('separator');
while(!($elm = $cont->getElement($locId))){
if(($i = strrpos($locId, $sep)) === false){
$locId = substr($locId, 0, $i);
if(($i = strpos($id, $sep)) === false){
return $this->recursiveGetElement(substr($id, $i+ 1), $elm);
* All children of this form inheret their IDs from it.
* When {@link parent::getGlobalId()} is called on a child, it calls its
* parent's getGlobalId() method and appends the results to its own
* Local ID. When the calling reaches the SForm level, it's at the root and
* there are no more parents, so the Local ID is returned.
* @return string The Local and Global ID
* Returns a nested array of all returned values
* Uses the default renderer to collect the values of all elements into an
* array. If objects are nested, then the array is nested also.
* @return array Values of this form in an array.
return $rend->fetch($this);
* Was this form submitted?
* @return boolean Has it been submitted?
//Use the tracking element
//This is how we track the form without tracking it
return !empty($this->request);
* Set the default values of local elements
* This is here for HTML_QuickForm compatability. Please use
* {@link SForm_Element::setDefault()} instead. The reasoning is that all
* operations that are associated with perticular elements should operate on
* those elements, so this usage makes more sense.
* Also, when using QF, I found myself simply collecting values into an
* array at the same layer as element creation. After the the array was
* completed, I had to pass it through various layers of my program to
* finally set the defaults. Using {@link SForm_Element::setDefault()} makes
* Internally, element defaults are stored and set in {@link render()} using
* the {@link SForm_Element::setDefault()} method at render time. Any calls
* to {@link SForm_Element::setDefault()} will be overridden by the
* values in this form. The elements are retrieved using {@link }
* getElement()}. This is due to the internal differences between QF and
* SForm. It doesn't mesh exactly with QuickForm, and you have been warned.
* Maybe you should use {@link SForm_Element::setDefault()} instead.
* @param array List of defaults for the elements.
* @see SForm_Element::setDefault()
foreach($defaults as $id => $def){
* Returns an element's value
* @param string Element to retrieve value for
* @return string Element's value
throw new Exception('The element "'. $name. '" wasn\'t found.');
* Add a rule to given field(s)
* Please use {@link addRule()} instead. This is only here for compatability
* @param mixed Element name(s)
* @param string Error message
* @param string Type of rule
* @param string Extra data
* @param string Where to preform validation
public function addRuleDep($element, $message, $type, $format = '', $validation = 'server'){
foreach($element as $elmId){
throw new Exception('Bad element name "'. $elmId. '"');
throw new Exception('Bad element name "'. $element. '"');
if($validation == 'server'){
$rule = $baseElm->createRule($type, $message, $elms, $format);
$rule->validateOnClient(false);
$baseElm->addRule($rule);
$baseElm->addRule($type, $message, $elms, $format);
* Renders a form with the provided renderer
* Render the form using the given {@link SForm_Renderer renderer}. If any
* defaults have been applied using {@link setDefaults()}, they are applied
* to the local elements here.
* @param object The renderer to use
public function render(SForm_Renderer $rend){
foreach($this->defaults as $id => $def){
throw new Exception('The element "'. $id. '" wasn\'t found when ' .
'we were trying to apply it\'s default "'. $def. '".');
* Validates this form. Keep in mind that no rules can be added after an
* element is validated because that would invalidate the validation. If
* there was no request submitted, it is assumed that validation is not
* Rules can be added via {@link addRule()}.
* @return boolean Is this form valid?
* @uses parent::validate()
* Prints this form to std out
* @param object Renderer to use
public function display($rend = null){
throw new Exception('Passed renderer is not a subclass of ' .
echo $rend->fetch($this);
* This is not currently working. Please {@link mailto:}
* sprice@pricepages.org request} if needed.
throw new Exception('FIXME');
* Process the results of the form
* This method is depreciated because it takes less code and you gain more
* functionality if you just call the callback function yourself. Use {@link }
* toArray()} to get the array, then call the function. That simple.
* @param mixed Callback function
public function process($callback){
* Create a group and add it to this form
* This is depreciated because you can just do this yourself by
* {@link SForm_Group creating a group} (or {@link SForm_Fieldset fieldset})
* and adding to the form via {@link SForm:: addElement()}. It's less
* @param array Elements in group
* @param string Group name
* @param string Group label
* @return object Added group
public function addGroup($elements, $id = null, $label = null){
foreach($elements as $elm){
* Returns a value from the request
* @param string Value to retrieve
* @return mixed Value or null
if(isset ($this->request[$val])){
return $this->request[$val];
* Temp method for testing
function smartyFetch($file, $addlVars = null){
$tpl = new ECSmarty(false);//Disable Smarty cache
$arr = $rndr->fetch($this);
return $tpl->fetch($file);
* @param boolean This isn't used here
* @return string The label
return self::escHtml($lbl);
* @return string Nothing. It's a boring group.
* Close this element group
* @return string Nothing. It's a boring group.
* Group elements in a '<fieldset>'
* Groups a set of elements in '<fieldset>' tags. All XHTML 1.1 compliant
* scripts must have elements seperated from their surrounding form by tags such
* as the fieldset. Also, it is pleasing to the eye and {@link http://www.webstandards.org/learn/tutorials/accessible-forms/intermediate/ more}
* accessable} to use fieldsets to group associated elements.
* Use the {@link __construct() label in the constructor} to create a legend for
* this fieldset. The legend will be rendered in the html label, using {@link }
* You can group fieldsets within each other, so feel free to use them with
* By default, render this as a fieldset
protected $tag = 'fieldset';
* Return the label as a legend tag
* Place a legend on this fieldset. The legend should be contained inside
* @param boolean This isn't used here
* @return string The label as a legend tag
return '<legend>'. self::escHtml($lbl). '</legend>';
* Close this element group
* @return string HTML that closes the group
return '</'. $this->tag. '>';
* Create rules for elements
* The SForm rules are very different than the QuickForm rules. Their advantages
* over QuickForm rules are twofold: They are designed to easily validate any
* number of elements simultaneously. They are also designed to throw an error
* (if needed) at any location in the form.
* This means that you can easily create complex rules, such as validating a
* credit card number depnding on which type of credit card is selected. You can
* require a user to fill in identifying information if they don't have a user
* name. You can better control filling out billing vs. shipping information.
* You can also do any of this in JavaScript.
* Much of this can even be done without defining a new class by using the
* {@link SForm_Rule_Boolean} to create an on-the-fly server and client side
* Because of the changes, the API is different. For example, this code from
* the original quick start guide:
* $form->addElement('text', 'name', 'Enter your name:',
* array('size' => 50, 'maxlength' => 255));
* $form->addRule('name', 'Please enter your name', 'required', null,'client');
* Can be changed into this, for the same functionality:
* $form->addElement('text', 'name', 'Enter your name:',
* array('size' => 50, 'maxlength' => 255));
* $form->addRuleDep('name', 'Please enter your name', 'required', null,'client');
* Note that in the example, addRule() has been changed to {@link }
* SForm::addRuleDep() addRuleDep()}, where 'Dep' stands for
* The correct SForm usage, though, is (shorthand):
* $nameElm = $form->addElement('text', 'name', 'Enter your name:',
* array('size' => 50, 'maxlength' => 255));
* $nameElm->addRule('required', 'Please enter your name');
* Or even (fully object oriented):
* $nameElm = new SForm_Elm_Text('name', 'Enter your name:',
* array('size' => 50, 'maxlength' => 255));
* $reqRule = new SForm_Rule_Required('Please enter your name', $nameElm);
* $nameElm->addRule($reqRule);
* $form->addElement($nameElm);
* Has this rule been asigned to an element?
* Should we use this rule with JavaScript?
* The message that is returned when an error is thrown. {@link toHtml()}
* can be overridden and the message made more dynamic, if desired.
* Which elements fall under this jurisdiction?
* The names of local JavaScript variables
* To save cycles and bytes, values are stored in local JavaScript variables
* before they are used in client side validation. This array is indexed by
* the Global ID, and the values are the names of the local variables.
* The counter for temp local variables
* Construct a rule with one element and one error message
* The first argument is always the error message, and the second argument
* is always an element or array of elements (as appropriate).
* The elements passed here are not necessarily the element that validates
* this rule and throws an error message. This way, the rule can
* validate two or more fields in a group, but the error appears in the
* group. This rule will later be added to said element or container with
* {@link SForm_Element::addRule()}.
* By default, the message is static, but you are encouraged to create
* dynamic messages that address the specific error and element via {@link }
* SForm_Element::getValue()} and {@link SForm_Element::getLabel()}
* respectively. Make sure, though, that messages still work on both the
* server side and client side.
|