An MVC Framework
An MVC Framework

The Model Class


Introducing Metadata

Every data object has a list of the data it will store or process. This is the metadata of that data object. In an SQL database it's expressed as it's schema. In this Framework it's expressed in a model.metadata.config file. Here's what one looks like:

#
#	object|db|dbname,tablename
#	object|order|default
#	object|field|fldname|type,len,required,unique,default,mask,key,min,max
#	object|link|object|type,fldname,lnk_fld,lazyload
#	object|pkey|fieldname,type
#
SalesOrder|db|myaccountingdb,salesorders
SalesOrder|pkey|ord_no,integer
SalesOrder|order|ord_no
SalesOrder|field|ord_no|integer, null, true, true, null, null, true, null, null
SalesOrder|field|_customer_id|integer, null, true, false, null, null, null, null, null
SalesOrder|field|source|varchar, null, true, false, null, null, null, null, null
SalesOrder|field|src_ordno|varchar, null, false, false, null, null, null, null, null
SalesOrder|field|ord_dt|timestamp, null, true, false, null, null, null, null, null
SalesOrder|field|discount_code|varchar, null, false, false, null, null, null, null, null
SalesOrder|field|discount_amt|numeric, 12.2, false, false, null, null, null, null, null
SalesOrder|field|subtotal|numeric, 12.2, false, false, 0.00, null, null, null, null
SalesOrder|field|tax|numeric, 12.2, false, false, 0.00, null, null, null, null
SalesOrder|field|weight|numeric, 6.2, false, false, 0.00, null, null, null, null
SalesOrder|field|shipping_info|varchar, null, false, false, null, null, null, null, null
SalesOrder|field|shipping|numeric, 12.2, false, false, 0.00, null, null, null, null
SalesOrder|field|pmt_method|varchar, null, false, false, null, null, null, null, null
SalesOrder|field|pmt_amt|numeric, 12.2, false, false, 0.00, null, null, null, null
SalesOrder|field|pmt_fee|numeric, 12.2, false, false, 0.00, null, null, null, null
SalesOrder|field|refund_note|varchar, null, false, false, null, null, null, null, null
SalesOrder|field|refund_amt|numeric, 12.2, false, false, null, null, null, null, null
SalesOrder|field|ord_status|varchar, null, false, false, Open, null, null, null, null
SalesOrder|field|listing_info|varchar, null, false, false, null, null, null, null, null
SalesOrder|field|notes|varchar, null, false, false, null, null, null, null, null
SalesOrder|field|_create_ts|timestamp, null, false, false, null, null, null, null, null
SalesOrder|field|_create_user_id|int4, null, false, false, null, null, null, null, null
SalesOrder|field|_modify_ts|timestamp, null, false, false, null, null, null, null, null
SalesOrder|field|_modify_user_id|int4, null, false, false, null, null, null, null, null
SalesOrder|link|InvTrans|1:n,Items,ord_no,false
SalesOrder|link|Customer|n:1,Customer,_customer_id,false
		

I'm not going to actually explain this file now. Mostly because it is one of the components of the Framework that is in flux. While these config files started as a quick and easy solution, they have become quite annoying to maintain over an entire application. And another design principle I hold to is "Don't Annoy Yourself." So a better format is being designed that will be less error prone and will dovetail nicely into the database schema generation as well. Regardless of what format is ultimately chosen it will result in the same global $metadata variable, thus having no impact on the rest of the Framework.

This metadata file is not used directly. It is preprocessed to turn it into a multidimensional array that is then stored in the global variable $metadata. This $metadata variable contains all the metadata for all the data objects within an application. This includes information on the fields, their data type, default settings, limits, along with keys and links (relations) to other data objects and even which database they're stored in. It will be accessed by the Model class to validate data input and determine hierarchical relationships.

Here's what a typical metadata.config.inc file looks like, this one is for Inventory Transactions:

$metadata['InvTrans']['db']=array(
				'dbname' => 'mydb',
				'tablename' => 'invtrans'
				);
$metadata['InvTrans']['pkey']=array(
				'fieldname' => 'trn_no',
				'type' => 'integer'
				);
$metadata['InvTrans']['order']=array(
				'default' => 'trn_no'
				);
$metadata['InvTrans']['field']['trn_no']=array(
					'type' => 'integer',
					'len' => null,
					'required' => true,
					'unique' => true,
					'default' => null,
					'mask' => null,
					'key' => true,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['_trn_dt']=array(
					'type' => 'varchar',
					'len' => null,
					'required' => true,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['_trn_type']=array(
					'type' => 'varchar',
					'len' => null,
					'required' => true,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['_item_no']=array(
					'type' => 'varchar',
					'len' => null,
					'required' => true,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['_lnk_table']=array(
					'type' => 'varchar',
					'len' => null,
					'required' => true,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['_lnk_id']=array(
					'type' => 'integer',
					'len' => null,
					'required' => true,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['_ln_no']=array(
					'type' => 'integer',
					'len' => null,
					'required' => true,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['qty']=array(
					'type' => 'numeric',
					'len' => '12.4',
					'required' => true,
					'unique' => false,
					'default' => '0',
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['units']=array(
					'type' => 'varchar',
					'len' => null,
					'required' => true,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['unit_price']=array(
					'type' => 'numeric',
					'len' => '12.2',
					'required' => true,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['unit_cost']=array(
					'type' => 'numeric',
					'len' => '12.4',
					'required' => true,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['units_ratio']=array(
					'type' => 'numeric',
					'len' => '10.6',
					'required' => false,
					'unique' => false,
					'default' => '1.0',
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['status']=array(
					'type' => 'varchar',
					'len' => null,
					'required' => true,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['_create_ts']=array(
					'type' => 'timestamp',
					'len' => null,
					'required' => false,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['_create_user_id']=array(
					'type' => 'int4',
					'len' => null,
					'required' => false,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['_modify_ts']=array(
					'type' => 'timestamp',
					'len' => null,
					'required' => false,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['field']['_modify_user_id']=array(
					'type' => 'int4',
					'len' => null,
					'required' => false,
					'unique' => false,
					'default' => null,
					'mask' => null,
					'key' => null,
					'min' => null,
					'max' => null
					);
$metadata['InvTrans']['link']['PurchaseOrder']=array(
					'type' => 'n:1',
					'fldname' => 'PurchaseOrder',
					'lnk_fld' => '_lnk_id',
					'lazyload' => true,
					'lnk_table' => '',
					'lnk_table_fld' => '',
					'order_by' => ''
					);
$metadata['InvTrans']['lnk_fld']['PurchaseOrder']=array(
					'object' => 'PurchaseOrder',
					);
$metadata['InvTrans']['link']['SalesOrder']=array(
					'type' => 'n:1',
					'fldname' => 'SalesOrder',
					'lnk_fld' => '_lnk_id',
					'lazyload' => true,
					'lnk_table' => '',
					'lnk_table_fld' => '',
					'order_by' => ''
					);
$metadata['InvTrans']['lnk_fld']['SalesOrder']=array(
					'object' => 'SalesOrder',
					);
$metadata['InvTrans']['link']['Item']=array(
					'type' => 'n:1',
					'fldname' => 'Item',
					'lnk_fld' => '_item_no',
					'lazyload' => false,
					'lnk_table' => '',
					'lnk_table_fld' => '',
					'order_by' => ''
					);
$metadata['InvTrans']['lnk_fld']['Item']=array(
					'object' => 'Item',
					);
		

Why Metadata?

"... avoids confusion and delay." -Sir Topham Hat

A valid question is why have a metadata config? Why not just put the same information into the derived data objects? That's one way of doing it. A metadata config is another way. I chose the metadata config for a few simple reasons:

If you use a NoSQL database this metadata config can be very helpful. NoSQL databases are not actually schema-less. They have an implied schema, which doesn't require documentation. By using metadata in your application, you have documented your implied schema. And that is not a bad thing.


Incorporating Metadata

Before I extend the setData() function to incorporate the use of the metadata, I need to make changes to the Model class

At this point the Model class must become an abstract class, since there will be no metadata associated with it. And to support this connection to $metadata I'll be adding an intrinsic variable and some additional functions.

	abstract class Model {
		
		private $classname = ""		// name of this class
		private $state = 'new';		// intrinsic data
		private $clean = true;
			
		private $data = array();	// extrinsic data stored in associative array(s)
		private $changed = array();
		
		
		function __construct( ... ) {		// explicit constructor
		
			$this->Init();						// second stage construction
			
		}
		
		function Init() {		// Initialization - second stage of construction
		
			$this->classname = get_class($this);
			
			// initialize extrinsic data
			$this->data = array();
			$this->changed = array();
			
			// initialize intrinsic data
			$this->clean = true;
			$this->state = 'new';
			
			⋮
		}
		
		// accessor to this class' metadata
		function md() {
			return $GLOBALS['metadata'][$this->classname];
		}
		
		⋮
	}		

Now the extended setData()

	abstract class Model {
	
		⋮
		
		function setData( $varname, $value, ... ) {	// $varname is the variable or field name of the data
		
			$md = $this->md();	// get a pointer to the class' metadata
		
			// check that the $varname matches one of the data object's field names
			// ignore $varname if no match
			if( array_key_exists( $varname, $md["field"] ) ) {
			
				$this->data[$varname] = $value;
				$this->changed[$varname] = $value;
				$this->clean = false;
				
			}
			
		}
		
		⋮
	}	

Field Protection

Not all fields should automatically be accepted from html forms. Primary keys, for one, should be protected from being arbitrarily changed. In addition, fields that collect system data e.g. creation and modification timestamps, as well as foriegn key fields and possibly others. These fields need to be protected from user input. But perhaps, not always.

The setData(), along with $metadata, will be extended to provide this ability. Fields that are tagged 'protected' will not automatically be inputted. But, the Setter will have a flag to override this protection when you know explicitly you want to input or change them.

Specific Setters

There is also the time when you may want have a Setter for a specific field. It may be a very unique data type or conform to very unique formatting. Or (shudder to think) it may be an edge case. setData() should first check if a specific field Setter exists and call that function instead of using the default processing.

Now to add field protection and check for specific Setters.

Currently protected fields are define by prefixing the field name with an underscore '_'. This will be changed to a flag in the $metadata so field naming will no longer have this restriction. This prefixing was started to make reading schemas easier but became a problem when trying to retrofit the Framework to existing databases.

Primary Keys are also considered protected. They are set through their own specific functions/methods. These functions/methods are not accessed from forms processing or derived business logic unless you draw outside the lines. This is a small example of good default behaviour that is easily overridden without needing to understand the framework and without the fear of breaking the whole framework.

abstract class Model {

	⋮
	
	function setData( $varname, $value, $protected = true ) {	// $varname is the variable or field name of the data
	
		// check for "Setter" function first
		$settername = 'set' . ucfirst($varname);	// function name should look like 'setVarname($value)'
		if( method_exists($this, $settername)) {
			return $this->$settername( $value );
		}

		$md = $this->md();	// get a pointer to the class' metadata
	
		// check that the $varname matches one of the data object's field names
		// ignore $varname if no match
		if( array_key_exists( $varname, $md["field"] ) ) {
		
			// check this is NOT a protected field or primary key
			if( $protected && ((substr($varname, 0, 1) == "_") || ($varname == $md["pkey"]["fieldname"])) ) {
				// this field is protected and will not be set
				error_log("Not setting protected field: " . $varname);
				return false;
			}
			
			$this->data[$varname] = $value;
			$this->changed[$varname] = $value;
			$this->clean = false;
			
		}
		
	}
	
	⋮
}	

So far we can input data into our object from a request. But only the data we explicitly permit. We can protect certain fields from being inputted at all, yet giving ourselves the ability to override the protection when desired. And we can easily add specific Setters/Getters without having to know their existence in advance, yet call them from the generic setter.

What about sanitizing the data?

It seems to be a common guideline to perform some type of "sanitizing" on the incoming data. Things like striping HTML tags or SQL commands. I'm ignoring that guideline here. I want to allow the storage of exactly what the user inputted. For any number of reasons. Also, there is no guarantee that the data entering the database will pass through the setData() function. As a developer you should have the right to do whatever you want, wherever you want to do it inside the application. Now to protect against SQL injection, the database interface will escape out SQL commands prior to database insertion. And to prevent XSS attacks, the rendering routines will escape out any HTML in a data field. By doing this, I support the GIGO principle. Which is very handy when debugging user created problems. Also, I've made no constraints on what environments this Model is used in, or the types of data it will store. Just the opposite. Remember not all applications are web apps and this Model (via CGI for one) can easily be used by any other native application. So rather than trying to sanitize incoming data, I've chosen to preserve the data and give the framework the ability to protect itself from bad data at the appropriate choke points. Again, not all bad data comes from user input.



Back to Top
Top

© 2012 and beyond Lawrence L Hovind - All Rights Reserved