Two-Tiered Input Validation example

Validating a new event with a start time 1 hour from now, and end time 2 hours from now:
bool(false)

Validating an existing future event that has been modified to start sooner:
bool(false)

Validating an existing future event that has been modified to start half an hour in the past:
array(1) { [0]=> string(33) "Start time must be in the future." }

Validating an existing in-progress event (start time is in the past):
bool(false)

Source Code


<h1>Two-Tiered Input Validation example</h1>
<?php
    
/**
     * Simulates logic in a controller action
     */
    
function controllerAction() {
        
// event1 simulates a new event being created. pretend a form was submitted
        
$event1 = new Event();        
        
// event2 simulates an existing future event being pulled from the database for editing
        
$event2 = new Event(array('start' => time()+3600'end' => time()+7200));        
        
// event3 simulates an existing in-progress event being pulled from the database for editing
        
$event3 = new Event(array('start' => time()-3600'end' => time()+3600));
    
        
/**
         * PROCESSING EVENT1
         * The submitted form tells us the event should start 1 hour from now, and end 2 hours from now
         */
        
$event1->start time()+3600;
        
$event1->end time()+7200;
        echo 
'Validating a new event with a start time 1 hour from now, and end time 2 hours from now:<br>';
        
var_dump($event1->validate('creation'));
        
        
/**
         * PROCESSING EVENT2
         * The user modifies event2 to start a half hour earlier.
         */
        
$event2->start -= 1800;
        
$event2->end -= 1800;
        echo 
'<br><br>Validating an existing future event that has been modified to start sooner:<br>';
        
var_dump($event2->validate('updating'));
        
        
/**
         * PROCESSING EVENT2
         * The user futher modifies event2 to start half an hour ago. Remember, having a start time in the past
         * is a no-no unless the Event was already in progress!
         */
        
$event2->start time()-1800;
        
$event2->end time()+1800;
        echo 
'<br><br>Validating an existing future event that has been modified to start half an hour in the past:<br>';
        
var_dump($event2->validate('updating'));
        
        
/**
         * PROCESSING EVENT3
         * The user modifies an in-progress event (the start time will be in the past). The controller determines if the
         * event is in progress or not.
         */
        
echo '<br><br>Validating an existing in-progress event (start time is in the past):<br>';
        
var_dump($event2->validate('updating.inProgress'));
    }
    
    
/**
     * A simple application model for an Event
     */
    
class Event extends Model {
        
// technical validation rules
        
public $technical = array(
            
'start' => 'numeric',
            
'end' => 'numeric'
        
);
        
        public function 
init() {
            
// rules not contained in a context are "default"
            
$this->logical[] = 'endAfterStart';
            
            
/**
             * Context constructor takes the following arguments:
             * @param    name    Name of the context
             * @param    rules    An array of rules specific to the context-- should be names
             *                            of validation callback methods in the model.
             * @param    ignore    A list of rules to ignore from parent contexts. Defaults to an empty array.
             */
            
$this->logical['creation'] = new Context('creation', array('future'));
            
$this->logical['updating'] = new Context('updating', array('future'));
            
            
// context: updating.inProgress -- no additional rules, ignore 'future' rule
            
$this->logical['updating']->addContext('inProgress', array(), array('future'));
        }
                
        
/**
         * VALIDATION CALLBACK: Makes sure the start time is in the future.
         */
        
public function future() {
            if(
$this->start time())
                return 
false;
            else
                return 
'Start time must be in the future.';
        }
        
        
/**
         * VALIDATION CALLBACK: Makes sure the end time is after the start time.
         */
        
public function endAfterStart() {
            if(
$this->end $this->start)
                return 
false;
            else
                return 
'End time must be after start time.';
        }
    }    
    
    
    
/**
     * EVERYTHING BEYOND THIS WOULD BE "BEHIND-THE-SCENES"
     */    
    
    
    
    
    
    
    
    
    
    /**
     * Extremely simple validator class, only supports 1 rule per field, but that's
     * all we need for this demo. The check method returns false on success, or a
     * list of errors on failure.
     *
     * NOTE: All validation rule callbacks in this example return false on success,
     *       or an error message on failure.
     */
    
abstract class Validator {
        public static function 
check($object$context) {
            
$errors = array();
            
            
// technical validation first
            
foreach($object->technical as $field => $rule) {
                if(
is_string($field)) {
                    if(
$error self::checkRule($object->$field$rule))
                        
$errors[] = $error;
                }
            }
            
            
// now the logical validation
            
$rules = array();
            
$ignore = array();
            
            if(isset(
$context)) {
                
// determine context-specific rules
                
              
$context explode('.'$context);
              
$workingContext $object->logical[$context[0]];
              
              for(
$i 1$i count($context); $i++)
                  
$workingContext $workingContext->{$context[$i]};
              
// $workingContext now = the deepest embedded subcontext
              
              // work our way from the inside out, collecting all of the validation rules
              
while($workingContext instanceof Context) {
                  
$ignore array_merge($ignore$workingContext->ignore);
                  foreach(
$workingContext->rules as $rule) {
                      if(!
in_array($rule$ignore) && !in_array($rule$rules))
                          
$rules[] = $rule;
                  }
                  
$workingContext $workingContext->parent;
              }
            }
            
            
// add default context rules (anything not contained within a Context object)
            
foreach($object->logical as $rule) {
                if(
is_string($rule) && !in_array($rule$ignore) && !in_array($rule$rules))
                    
$rules[] = $rule;
            }
            
            
// finally, check the object against the rules
            
foreach($rules as $rule) {
                if(
$error self::checkRule($object$rule))
                    
$errors[] = $error;
            }
            
            return 
count($errors) ? $errors false;
        }
        
        
/**
         * Performs an individual rule check. If $value is an object, $value->$rule()
         * is called. Otherwise, it's a built-in rule.
         */
        
public static function checkRule($value$rule) {
            if(
$value instanceof Model)
                return 
$value->$rule();
            return 
call_user_func(array('Validator'$rule), $value);
        }
        
        
/**
         * A simple built-in validation rule-- checks to see if the value is numeric.
         */
        
public static function numeric($value) {
            return 
is_numeric($value) ? false 'Not a valid integer';
        }
    }

    
/**
     * A simple overloadable Model class for use in this test
     */
    
class Model {
        private 
$data;
        private 
$changes;
        
        public 
$technical;
        public 
$logical;
        
        public function 
__construct($data = array()) {
            
$this->data $data;
            
$this->init();
        }
        public function 
__get($property) {
            if(isset(
$this->changes[$property]))
                return 
$this->changes[$property];
            else
                return 
$this->data[$property];
        }
        public function 
__set($property$value) {
            
$this->changes[$property] = $value;
        }
        
        public function 
save() {
            
$this->data $this->changes $data;
            
$this->changes = array();
        }
        
        public function 
validate($context NULL) {
            return 
Validator::check($this$context);
        }
    }
    
    
/**
     * Holds validation rules for a specific context, and allows for ignoring of
     * validation rules in parent contexts.
     */
    
class Context {
        public 
$name;
        public 
$rules = array();
        public 
$ignore = array();
        public 
$parent;
        public 
$subcontexts = array();
        
        public function 
__construct($name$rules$ignore = array(), $parent NULL) {
            
$this->name $name;
            
$this->rules $rules;
            
$this->ignore $ignore;
            
$this->parent $parent;
        }
        public function 
addContext($name$rules$ignore = array()) {
            
$this->subcontexts[$name] = new Context($name$rules$ignore$this);
        }
        public function 
__get($name) {
            return 
$this->subcontexts[$name];
        }
    }
    
    
controllerAction();
    echo 
'<h2>Source Code</h2><pre style="background: #eee; border: 1px solid #ccc">',highlight_file('2tvalidation.php',true),'</pre>';