An MVC Framework
An MVC Framework

The Model Class


Validation

Even though data sanitizing isn't performed on the input, that doesn't mean data isn't validated. To perform this, two functions are added to the Model class: Validate() and ValidateField().

ValidateField()

As the name implies ValidateField() will validate a single value in one field. It takes the field name and value as arguments. It looks up the field name in the $metadata to determine the field's type and then checks $value for validity. It returns the status or with some data types it returns a modified value.

This function only checks for validity. Additional steps are needed to record the status in the object's DataMaps. This behavior can be very handy for things like date fields. The user can enter "today" as a value and this function will convert that into a specific date and standard format.

abstract class Model {

	⋮
	
	function ValidateField( $varname, $value ) {

		$md = $this->md();
		$fld_md = $md['field'][$varname];

		if( $fld_md["required"] && !strlen($value) ) {	// if field is required but $value is blank
			return "REQUIRED";							// return with status
		}

		switch( $fld_md['type'] ) {
			case 'varchar' :
				return 'OK';
			case 'int4' :
			case 'int8' :
			case 'integer' :
				if( is_numeric($value) && is_int($value+0) ) {
					if( $fld_md['min'] && ($value < $fld_md['min']) ) {
						return 'LESS_THAN_MIN';
					}
					if( $fld_md['max'] && ($value > $fld_md['max']) ) {
						return 'MORE_THAN_MAX';
					}
					return 'OK';
				} else {
					if( $value != null ) {
						return 'NOT_INTEGER';
					} else {
						return 'OK';
					}
				}
				break;
			case 'double' :
			case 'numeric' :
			case 'decimal' :
				if( is_numeric($value) ) {
					if( $fld_md['min'] && ($value < $fld_md['min']) ) {
						return 'LESS_THAN_MIN';
					}
					if( $fld_md['max'] && (value > $fld_md['max']) ) {
						return 'MORE_THAN_MAX';
					}
					return 'OK';
				} else {
					if( $value != null ) {
						return 'NOT_NUMBER';
					} else {
						$value = null;
						return 'OK';
					}
				}
				break;
			case 'date' :
				if( strlen($value) ) {
					$tmptime = strtotime( $value );
					if( $tmptime != false ) {
						$value = strftime( '%Y-%m-%d', $tmptime );
						return $value;
					} else {
						return 'NOT_DATE';
					}
				} else {
					return "OK";
				}
				break;
			case 'timestamp' :
				if( strlen($value) ) {
					$tmptime = strtotime( $value );
					if( $tmptime != false ) {
						/*
						$value = strftime( '%Y-%m-%d %H:%M:%S %z', $tmptime );
						return $value;
						*/
						return "OK";
					} else {
						return 'NOT_TIMESTAMP';
					}
				} else {
					return "OK";
				}
				break;
			case 'email' :
					if( ($value == "") || validEmail($value) ) {
						return "OK";
					} else {
						return "NOT_VALID_EMAIL";
					}
				break;
			case 'money' :
				if( is_numeric($value) ) {
					if( $fld_md['min'] && $value < $fld_md['min'] ) {
						return 'LESS_THAN_MIN';
					}
					if( $fld_md['max'] && $value > $fld_md['max'] ) {
						return 'MORE_THAN_MAX';
					}
					return 'OK';
				} else {
					if( $value != null ) {
						return 'NOT_MONEY';
					} else {
						return 'OK';
					}
				}
				break;
			case 'bool' :
				if( is_bool($value) ) {
					return 'OK';
				} else {
					if( is_string($value) ) {
						switch ($value) {
							case 'F' :
							case 'f' :
							case 'T' :
							case 't' :
							case '0' :
							case '1' :
								return "OK";
								break;
							default :
								break;
						}
					}
				}
					return 'NOT_BOOLEAN';
				break;
			default :
				break;
		}
		return 'UNKNOWN_TYPE';

	}
	
	⋮
}
	

Adding in a check for field specific Validators is simple enough. It would follow the same format as the Setter/Getters, i.e. ValidateVarname( $value ). I'll probably add this in when the need arises.

Validate()

The Validate() function will validate all the fields in an object.

Validate() does take a list of field names to exclude from validation. This is particularly helpful when validating new objects from html forms. If a Model object is populated over a series of html forms (as in the case of a Wizard), you can check for validity at each step without causing unpopulated fields to be prematurely flagged as invalid. Likewise, the primary key is automatically excluded as it will most likely not come from user data, but will be generated by the Save() function.

abstract class Model {

	⋮
	
	function Validate( $exclude_flds = array() ) {
	
		$md = $this->md();
		
		array_unshift( $exclude_flds, $md["pkey"]["fieldname"]);

			$this->invalid = array();

			foreach( $md['field'] as $fldnm => $fld_md ) {
	
				if( in_array($fldnm, $exclude_flds) ) {	// skip if this is an excluded field
					continue;
				}
				
				$res = $this->ValidateField($fldnm, $this->getData($fldnm) );
	
				// process the status or results of the ValidateField()
				switch( substr($res,0,4) ) {
					case 'NOT_' :
					case 'LESS' :
					case 'MORE' :
					case 'REQU' :
					case 'UNKN' :
						$this->invalid[$fldnm] = $res;
						break;
					case 'OK' :
						unset($this->invalid[$fldnm]);	// remove from invalid DataMap
						break;
					default :
						$this->setData($fldnm, $res);
						unset($this->invalid[$fldnm]);
						break;
				}
	
			}
			
		if( count($this->invalid) ) {
			return false;
		} else {
			return true;
		}
	}
	
	⋮
}
	

Validating Relationships

Because of relationships, an object might only be valid if all of it's linked objects are also valid. (e.g. a Sales Order must have valid Inventory Transactions and a valid Customer.) For this reason Validate() needs to also validate any linked objects.

Because all memory objects are dual linked, there must be a way to prevent recursion during validation or we enter an infinite loop. To this end an object pointer is placed in the GLOBAL variable 'in_validate'. This way the Validate() function can tell if a object is already being validated on some level and will avoid recursion.

Linked objects fall into two different categories: dependent and independent. Basically following the "1:n" and "n:1" relationship models. The basic path of Validate() is to validate all the independent links first, then the object's data fields and then the dependent links.

abstract class Model {

	⋮
	
	function Validate( $exclude_flds = array() ) {
	
		$md = $this->md();
		
		array_unshift( $exclude_flds, $md["pkey"]["fieldname"]);

		if( !in_array( $this, $GLOBALS["in_validate"]) ) {	// object is not in being validated list - this avoids recursion
			
			// add object to being validated list
			$GLOBALS["in_validate"][] = &$this;
			
			$this->invalid = array();
			$links = @$md["link"];
			
			// Validate all independent links first
			
			if( is_array($links) ) {
				foreach( $links as $objclass => $lnk_md ) {
					if ( isset($this->data[$lnk_md["fldname"]]) ) {	// this was inserted to avoid problems
																	// with objects that have a 'pick one of many'
																	// possible independent links
						switch( $lnk_md["type"] ) {
							case "1:1" :
							case "n:1" :
								$res = $this->data[$lnk_md["fldname"]]->Validate();
								if( $res ) {
									// add foreign key to exclusion list if object is new/unsaved
									if( ($this->data[$lnk_md["fldname"]]->getID() == null) && ($this->data[$lnk_md["fldname"]]->state == "new") ) {
										array_unshift($exclude_flds, $lnk_md["lnk_fld"]);
									}
								} else if( !in_array( $this->data[$lnk_md["fldname"]], $GLOBALS["in_validate"]) ) {
											// PROBLEM - if we skip an object because it's already 'in_validate' then
											// $res == false, BUT we don't need to add it's invalids to us again
											// so we test that the object is not in the in_validate global above
											
									// invalid fields in object, add to this->invalid
									$fldname = $lnk_md["fldname"];
									foreach( $this->data[$lnk_md["fldname"]]->invalid as $key => $value ) {
										$this->invalid[$fldname . ":" . $key] = $value;
										// notice the cascade notation
									}
								}
								break;
							default :
						}
					}
				}
			}
			
			// Now Validate this object ***************************************************************
			foreach( $md['field'] as $fldnm => $fld_md ) {
	
				if( in_array($fldnm, $exclude_flds) ) { // skip if this is an excluded field
					continue;
				}
				
				$res = $this->ValidateField($fldnm, $this->getData($fldnm) );
	
				// process the status or results of the ValidateField()
				switch( substr($res,0,4) ) {
					case 'NOT_' :
					case 'LESS' :
					case 'MORE' :
					case 'REQU' :
					case 'UNKN' :
						$this->invalid[$fldnm] = $res;
						break;
					case 'OK' :
						unset($this->invalid[$fldnm]);	// remove from  the invalid DataMap
						break;
					default :
						$this->setData($fldnm, $res);
						unset($this->invalid[$fldnm]);
						break;
				}
	
			}
			// **************************************************************************************
			
			// Validate all dependent links 1:n
			if( is_array($links) ) {
				foreach( $links as $objclass => $lnk_md ) {
					if( ($lnk_md["type"] == "1:n") && isset($this->data[$lnk_md["fldname"]]) ) {
						$excludes = array();
						if( ($this->getID() == null) && ($this->state == "new") ) {
							// the 'lnk_fld has to be pulled from the objects md()
							array_unshift($excludes, $lnk_md["lnk_fld"]);
							// if 'this' is new/unsaved then exclude its ID in dependent objects
						}
						foreach( $this->data[$lnk_md["fldname"]] as $idx => $obj ) {
							$res = $this->data[$lnk_md["fldname"]][$idx]->Validate($excludes);
							if( $res ) {
								// nothing to do, everything is good
							} else {
								// invalid fields in object, add to this->invalid
								$fldname = $lnk_md["fldname"];
								foreach( $this->data[$lnk_md["fldname"]][$idx]->invalid as $key => $value ) {
									$this->invalid[$fldname . "(" . $idx . ")" . ":" . $key] = $value;
									// notice the cascade notation
								}
							}
						}
					}
				}
			}
			
			
			// remove object from being validated list
			$idx = array_search( $this, $GLOBALS["in_validate"]);
			unset($GLOBALS["in_validate"][$idx]);
				
			
		}
		
		if( count($this->invalid) ) {
			return false;
		} else {
			return true;
		}
	}
	
	⋮
}
	

Upon return from Validate() an object's $data->invalid[] will contain a list of the invalid fields and the reasons they are invalid. The field names should be in the same form as described in Cascading Setter/Getters. This is useful for error reporting in html forms. As the rendering routines are designed to flag the <input> fields based on the keys of the $data->invalid[] array.



Back to Top
Top

© 2012 and beyond Lawrence L Hovind - All Rights Reserved