name = $name; $this->title = $title; $this->value = NULL; $this->validator = $validator; $this->allow_multi = $allow_multi ? true : false; #in case NULL is passed $this->max_length = $max_length; $this->required = false; $this->error_msgs = array(); } function getMaxLength(){return $this->max_length;} function getTitle($strict = false){ #strict means, if there is no $title set, NULL should be returned rather than the $name return isset($this->title) ? $this->title : ($strict ? NULL : $this->name); } function setErrorMsg($error_type, $message){ #for instance, FORMATION_FIELD_DISALLOWED => 'must be an e-mail address' #you can reset a custom error message by passing a $message of NULL if(is_null($message)) unset($this->error_msgs[$error_type]); else $this->error_msgs[$error_type] = $message; } function getError(){ if(!$this->error_type) return NULL; #if no error, return NULL if(isset($this->error_msgs[$this->error_type])) $msg = $this->error_msgs[$this->error_type]; else switch($this->error_type){ case FORMATION_FIELD_REQUIRED: $msg = FORMATION_FIELD_REQUIRED_MSG; break; case FORMATION_FIELD_INVALID: $msg = FORMATION_FIELD_INVALID_MSG; break; case FORMATION_FIELD_DISALLOWED: $msg = FORMATION_FIELD_DISALLOWED_MSG; break; } $title = $this->getTitle(); return "'$title' $msg"; } function doesAllowMulti(){return $this->allow_multi;} function hasError(){return $this->error_type != 0;} function defaultValue($value = NULL){ #value can be an array if allow_multi #note, default_value can be a value outside of possible_values # and is not subject to the validator. # The default value is a "free for all", nothing can stop it! Roar! if(is_null($value)) return $this->default_value; if(is_array($value) and !$this->allow_multi) return false; $this->default_value = $value; return true; } function getPossibleValues(){ #remember, $validator contains the array of possible values, if possible values are set return is_array($this->validator) ? $this->validator : NULL; } function required($bool = NULL){ #note that required and a default value is silly... since if a default value is provided # the field will *always* have some value, so required won't really do anything if(is_null($bool)) return $this->required; #just return the current value $this->required = $bool; } function get($get_default = true){ #gets the value for a field #if you specify a $get_default of false, then it will only # return the value submitted, and not substitute the default value return (!isset($this->value) and $get_default and isset($this->default_value)) ? $this->default_value : $this->value; } function set($value, $bypass_error_check = false){ $valid = true; if(!$bypass_error_check){ $valid = false; if(!$value){ $valid = !$this->required; if(!$valid) $this->error_type = FORMATION_FIELD_REQUIRED; }else{ #sanity check. Value can't be an array if allow_multi is off. #maybe I should do something better than just setting it to NULL. # However, since this should only happen in the case where someone's # screwing with the form, I'll leave it like this for now if(is_array($value) and !$this->allow_multi) $value = NULL; if(!$this->validator) $valid = true; elseif(is_array($this->validator)){ #enumerated possible values $possible_values = array_keys($this->validator); $valid = is_array($value) ? count(array_diff($value, $possible_values)) == 0 : in_array($value, $possible_values); if(!$valid) $this->error_type = FORMATION_FIELD_DISALLOWED; }else{ $valid = is_callable($this->validator) ? call_user_func($this->validator, $value) : preg_match($this->validator, $value); if(!$valid) $this->error_type = FORMATION_FIELD_INVALID; } } } if(is_bool($value)) $value = $value ? 1 : 0; $this->value = $value; return $valid; } } ################################################################################### # ActionForm ################################################################################### class ActionForm{ var $fields, #associative array of field objects, keyed by field name $errors, #an array of error messages specific to the form, but not to any field $message, #one overall message indicating the result of the form, #such as "Saved your comment", or "Couldn't update your entry" $success, #indicates whether the form was submitted successfully $name, #the name of the entire action form $results #the place to store anything you want that was generated during process() ; function ActionForm($context = NULL){ #$context represents any data the form needs to "do its job" # Context might contain a database connection, a list of default fields, # some parameter, etc. It's meant to be used by the construct() function. $this->name = strtolower(get_class($this)); #strtolower because PHP 4,5 handle the case of get_class differently. $this->reset($context); } function reset($context = NULL){ $this->fields = array(); $this->errors = array(); $this->data = array(); $this->message = NULL; $this->success = NULL; #starts out as null, since it is neither false nor true if($this->construct($context) === false){ $die_message = "Could not construct action '$this->name'"; if($this->message) $die_message .= ': '.$this->message; die($die_message); #not sure if I want to die here or what } if(!array_key_exists('submit', $this->fields)) $this->addSubmit(); #hmm... do forms *need* a submit button? I've seen them without. } function construct($context){ #this function must be overriden in every Action subclass. # It defines the structure of the form } function init(&$fields){ #if init is overridden in a subclass, # this version if init should be called first at the beginning of that function $success = true; foreach($fields as $field_name=>$value) if(!$this->set($field_name, $value, true)) $success = false; return $success; } function submit($data = NULL){ $this->coerce($data); if($result = $this->populate($data) and $result = $this->validate()) $result = $this->process(); return $this->success = (bool)$result; } function coerce(&$data){ #this function will coerce the provided action data, uppercasing, combining fields, # creating calculated fields from other fields... whatever you need to do } function populate(&$data){ #on submit, populates the data provided in $this->data into the field values # and does some basic sanity checks on the data according to what's supposed # to be valid for each field $fields = array_keys($this->fields); $return = true; foreach($fields as $field){ $result = $this->fields[$field]->set(isset($data[$field]) ? $data[$field] : NULL); if(!$result) $return = false; } return $return; } function validate(){ #this function may be overriden in an Action subclass. # It checks for the correct structure of the form fields # Must return a boolean indicating whether the form validated or not. #this function is for validating inter-field relationships return true; } function process(){ #this function must be overriden in every Action subclass. # It does the actual "processing" involved in dealing with a # *correctly* submitted form. This messes with databases, etc. echo "Default process!\n"; } function add(&$field){$this->fields[$field->name] = &$field;} function addError($message){$this->errors[] = $message;} function success(){return $this->success === true;} function submitted(){return !is_null($this->success);} function getSubmitValues(){return array_flip($this->fields['submit']->getPossibleValues());} function & getFields(){return $this->fields;} function getRequiredFields(){ #returns a hash of all fields that are required, field names are keys return array_filter($this->fields, create_function('$a','return $a->required();')); } function get($field, $get_default = true){ return isset($this->fields[$field]) ? $this->fields[$field]->get($get_default) : NULL; } function getMany($fields){ $results = array(); foreach($fields as $field) $results[$field] = $this->get($field); return $results; } function set($field, $value, $bypass_error_check = false){ return isset($this->fields[$field]) and $this->fields[$field]->set($value, $bypass_error_check); } function getFieldValues($get_default = false){ $result = array(); $fields = array_keys($this->fields); foreach($fields as $field) $result[$field] = $this->get($field, $get_default); return $result; } function getErrors(){ #returns an array containing the main errors, and the errors for each field $field_errors = array(); $fields = array_keys($this->fields); foreach($fields as $field) if(isset($this->fields[$field]) and $error = $this->fields[$field]->getError()) $field_errors[$field] = $error; return array('form'=>$this->errors, 'fields'=>$field_errors); } function addSubmit($submit = NULL){ #because of the way the validator works - the *keys* for the possible values in the validator are the actual values #however, the value="" for the submit button should be the value, not the key #however, when the set() function validates each field, it does it according to the key. #EXPLAIN THIS BETTER $field = &new ActionField('submit',NULL,is_array($submit) ? array_flip($submit) : array($submit=>0)); $this->add($field); } function message($value = NULL){ #sets or returns the form message if(is_null($value)) return $this->message; $this->message = $value; } function getResults(){ return $this->results; } } ################################################################################### # Formation ################################################################################### class Formation{ var $action, $field_settings, #settings which help dictate how a field is rendered #possible settings: #widget_type: determines the HTML forms widget used to represent this field #size: determines the maximum allowed text to go into a field #aux_text: extra text to go after a text label $default_multi_count = 5, #default number of possible elements something has #to have before Formation renders it as a dropdown by default instead #of a series of checkboxes or radio buttons $invalid_css_class = 'Formation-invalid', #the CSS class appended to elements that have errors $widget_types, #an array that organizes the widget types by class $widget_methods, #an array that maps widget types to method names $widget_styles, #a mapping of styles to widget types that are printed in that style $prg_data, #holds any data that's transmitted on the tail end of a PRG $location #the URL to post to ; function Formation(&$action, $context = NULL, $submit=true){ $this->method = 'post'; if(!$action) die("Formation: No action given"); elseif(is_string($action)) $this->action = &new $action($context); else $this->action = &$action; if(isset($_REQUEST['action']) and $_REQUEST['action'] == $this->action->name){ #note that this copies the data in $_REQUEST #by the way, I hate magic_quotes with a passion $this->action->submit(get_magic_quotes_gpc() ? array_map('stripslashes',$_REQUEST) : $_REQUEST); } $cookie_name = $this->getPrgCookieName(); if(isset($_COOKIE[$cookie_name])){ if(get_magic_quotes_gpc()) $_COOKIE[$cookie_name] = stripslashes($_COOKIE[$cookie_name]); $this->prg_data = @unserialize($_COOKIE[$cookie_name]); setcookie($cookie_name, NULL); #delete cookie } $this->field_settings = array(); #pre-determine the widget types of all fields $fields = &$this->action->getFields(); $field_names = array_keys($fields); foreach($field_names as $field_name){ $f = &$fields[$field_name]; $fs = &$this->field_settings[$field_name]; if(is_array($pv = $f->getPossibleValues())){ if($field_name == 'submit'){ #special case $fs['widget_type'] = FORMATION_SUBMIT; }else{ $c = count($pv); $multi = ($f->doesAllowMulti() or $c == 1) ? FORMATION_CHECKBOX : FORMATION_RADIO; $fs['widget_type'] = ($c > $this->default_multi_count) ? FORMATION_DROPDOWN : $multi; } }elseif(!$f->getTitle(true)){ #a field with no title should be hidden by default $fs['widget_type'] = FORMATION_HIDDEN; }else{ $fs['widget_type'] = $f->getMaxLength() > 255 ? FORMATION_TEXTAREA : FORMATION_TEXT; } } # set up the widget classes $this->widget_types = array( 'single'=>array( FORMATION_TEXT, FORMATION_PASSWORD, FORMATION_TEXTAREA, FORMATION_HIDDEN, FORMATION_OMITTED, FORMATION_DROPDOWN ), 'multi'=>array( FORMATION_RADIO, FORMATION_CHECKBOX, FORMATION_DROPDOWN ) ); $this->method_names = array( FORMATION_TEXT => 'printText', FORMATION_PASSWORD => 'printText', FORMATION_TEXTAREA => 'printTextArea', FORMATION_CHECKBOX => 'printCheckbox', FORMATION_RADIO => 'printRadio', FORMATION_DROPDOWN => 'printDropdown', FORMATION_HIDDEN => 'printHidden', FORMATION_DUMP => 'printDump', FORMATION_SUBMIT => 'printSubmit' ); $this->widget_styles = array( 'label_goes_above'=>array( #label comes *above* the field FORMATION_TEXTAREA, FORMATION_RADIO, FORMATION_CHECKBOX, FORMATION_DROPDOWN ), 'hidden'=>array(FORMATION_HIDDEN, FORMATION_OMITTED), 'special'=>array(FORMATION_SUBMIT) ); } function markInvalid($field_name){ #private function if( $this->action->submitted() and isset($this->field_settings[$field_name]) and $this->action->fields[$field_name]->hasError() ){ echo ' class="'.htmlspecialchars($this->invalid_css_class).'"'; } } ############################################################################### # WIDGETS! ############################################################################### function printDump(&$field){ $value = $field->get(false); echo is_array($value) ? join(', ',array_map('htmlspecialchars',$value)) : htmlspecialchars($value); } ############################################################################### # printTextArea ############################################################################### function printTextArea(&$field, &$field_settings){ ?> size=""getMaxLength()){ ?> maxlength="" value="get(false)));?>" markInvalid($field->name); ?>/>FooBox($field, $field_settings, 'radio');} function printCheckbox(&$field, &$field_settings){$this->FooBox($field, $field_settings, 'checkbox');} function FooBox(&$field, &$field_settings, $type){ #type == 'checkbox' or 'radio' #meant as a convenience function for checkboxes or radio buttons $possible_values = $field->getPossibleValues(); $value = $field->get(false); #show($value); $arr = array(); $mult = $field->doesAllowMulti(); foreach($possible_values as $pv=>$desc){ $id = htmlspecialchars($field->name.'_'.$pv.'_id'); ob_start(); ?> checked="checked" /> printListInColumns($arr, $field_settings['size']); else $this->printListInColumns($arr); } ############################################################################### # printDropdown ############################################################################### function printDropdown(&$field, &$field_settings){ $possible_values = $field->getPossibleValues(); $option_count = count($possible_values); $mult = $field->doesAllowMulti(); $value = $field->get(false); ?> field_settings[$field_name])) return false; $f = &$this->action->getFields(); if(isset($f[$field_name]) and $title = $f[$field_name]->getTitle(true) and $this->field_settings[$field_name]['widget_type'] != FORMATION_HIDDEN){ ?>: field_settings[$field_name]['aux_text'])){ echo $this->field_settings[$field_name]['aux_text']; } return true; } return false; } ############################################################################### # field ############################################################################### function field($field_name, $tab_index = NULL){ #tab index not used yet if(!isset($this->field_settings[$field_name])){ echo 'No field with name '.htmlspecialchars($field_name)."\n"; return false; } $fs = &$this->field_settings[$field_name]; if(!isset($this->method_names[$fs['widget_type']])) return false; $method_name = $this->method_names[$fs['widget_type']]; $fields = &$this->action->getFields(); $field = &$fields[$field_name]; $this->$method_name($field, $fs); return true; } ############################################################################### # printSubmit ############################################################################### function printSubmit(&$field){ $pv = array_keys($field->getPossibleValues()); for($n=0,$c=count($pv); $n<$c; $n++){?> 1){?> name="submit" value="" /> location) return $this->location; return $this->method == 'post' ? $_SERVER['REQUEST_URI'] : $_SERVER['PHP_SELF']; } function beginForm($location = NULL, $css_class = NULL, $form_header = NULL){ if(is_null($location)) $location = $this->getDefaultLocation()?>

field_settings, create_function('$a','return $a[\'widget_type\'] == FORMATION_HIDDEN;'))); #print the action if($print_action) $this->printAction(get_class($this->action)); foreach($hidden_fields as $field_name) #print all hidden fields $this->field($field_name); ?>

printStatus(); $this->beginForm($location, $css_class); ?> action->getFieldValues()); foreach($fields as $field){ $type = $this->field_settings[$field]['widget_type']; if(in_array($type, $this->widget_styles['label_goes_above'])){ ?> widget_styles['special'])){?> widget_styles['hidden'])){?>
label($field)){ echo '
';} $this->field($field); ?>
field($field);?>
label($field);?> field($field);?>
endForm(); } ############################################################################### # printStatus ############################################################################### function printStatus($heading_level=1){ if($this->action->submitted()){ $message = $this->action->message(); if(!$this->action->success){ if(!$message) $message = FORMATION_ERROR_MSG; echo "$message"; ?> $message"; } } } ############################################################################### # printStatus ############################################################################### function printListInColumns($list, $colcount = 3, $func = 'print_r', $sortbycolumn = true, $class = NULL){?> class=""> method; if(!($method == 'get' or $method == 'post')) return false; $this->method = $method; return true; } function get($field, $get_default = true){return $this->action->get($field, $get_default);} function getMany($fields){return $this->action->getMany($fields);} function set($field, $value){return $this->action->set($field, $value);} function message($value = NULL){return $this->action->message($value);} function success(){return $this->action->success();} function submitted(){return $this->action->submitted();} function property($field_name, $property, $value = NULL){ if(!isset($this->field_settings[$field_name])) return false; if(is_null($value)) return $this->field_settings[$field_name][$property]; $this->field_settings[$field_name][$property] = $value; return true; } function tabindex($field_name, $index = NULL){return $this->property($field_name, 'tabindex', $index);} function disabled($field_name, $bool = NULL){return $this->property($field_name, 'disabled', $bool);} function readonly($field_name, $bool = NULL){return $this->property($field_name, 'readonly', $bool);} function size($field_name, $size = NULL){return $this->property($field_name, 'size', $size);} function widget($field_name, $widget = NULL){return $this->property($field_name, 'widget', $widget);} function setSize($field_name, $size){return $this->size($field_name, $size);} #backward compatibility function setWidget($field_name, $widget){ #the only thing that's invalid is that there are no possible values # and you want to make the field a radio button, checkbox, or dropdown $fields = &$this->action->getFields(); $field = &$fields[$field_name]; $pv = $field->getPossibleValues(); if( !isset($this->field_settings[$field_name]) or (!$pv and in_array($widget, $this->widget_types['multi'])) or (count($pv) > 1 and $widget == FORMATION_CHECKBOX and !$field->doesAllowMulti()) ){ #show("Tried to set widget for $field_name to: ".$this->method_names[$widget].' and it failed'); return false; } #show("Set widget for $field_name to: ".$this->method_names[$widget].' and it succeeded'); $this->field_settings[$field_name]['widget_type'] = $widget; return true; } function setAuxText($field_name, $text){ #allows you to add short text that appears next to the label return $this->property($field_name, 'aux_text', $text); } function getPrgCookieName(){ return FORMATION_PRG_COOKIE_NAME.'_'.$this->action->name; } function redirect($location=NULL, $data=NULL, $secure = false){ #this is a convenience method to implement the PRG pattern (POST->redirect->GET) #$location, if given, must be the full http location ("http://...") as required by HTTP if(!$location) $location = 'http://'.$_SERVER['HTTP_HOST'].$this->getDefaultLocation(); $temp = array( 'message'=>$this->message(), 'success'=>$this->success(), 'results'=>$this->getResults() ); if(!is_array($data)) $data = $temp; else $data = array_merge($temp, $data); header("Location: $location"); header('Connection: close'); setcookie($this->getPrgCookieName(), serialize($data),NULL,NULL,NULL,$secure); return true; } function getPrgData(){ if($this->prg_data) return $this->prg_data; else return NULL; } function isPrg(){ return isset($this->prg_data); } function getResults(){ return $this->action->getResults(); } function location($location = NULL){ $this->location = $location; } function init(&$fields){ return $this->action->init($fields); } } ?>