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>';