An MVC Framework
An MVC Framework

The Model Class


Hierarchy of Objects

Data is NOT flat. It's commonly hierarchical. (Sometimes even circular.) This is represented with Links. Under this framework Links are very easy. Links are implemented by adding another key-value pair to the DataMap for the objects linked. How these links are configured is easily defined in the $metadata.

The $metadata has an array of Link metadata for this object. The Link metadata tells us the type of object, the associative name it's stored as, the type of relationship (i.e. 1:1, 1:n, n:m ), the foreign key field used and whether to use 'lazyload'.

Lazyload is a flag indicating whether the secondary objects linked to a primary object should be retrieved when retrieving the primary object.

e.g. When retrieving a Sales Order it makes sense to also retrieve the Customer object that goes with that Sales Order. But, when retrieving a Customer it might not make sense to retrieve all the Sales Orders that Customer made. We may only want the contact info of the Customer and we don't need a sales history. For this reason, lazyload is set to true on the Customer's Sales Orders and the Sales Orders will only be retrieved when explicitly requested.

All Links are two way. This permits the traversing of the hierarchy from any object within the hierarchy.

And yes, this does create loops. Mostly harmless ones.

Using the linking is simple:

	$SalesOrder = new SalesOrder();
	$Customer = new Customer();
	
	$SalesOrder->Link( $Customer );
	// or
	$Customer->Link( $SalesOrder );
	
	// the two 'Link()' statements are equivalent
	

Here's the code that get's it done:

abstract class Model {

	⋮
	
	function Link( $object ) {

		$linkername = "Link_" . $object->classname;
		if( method_exists($this, $linkername)) {
			$this->$linkername($object);
		} else {
			$this->_addLink($object);
		}

		$linkername = "Link_" . $this->classname;
		if( method_exists($object, $linkername)) {
			$object->$linkername($this);
		} else {
			$object->_addLink($this);
		}
	}
	
	function UnLink($object) {
		
		$linkername = "UnLink_" . $object->classname;
		if( method_exists($this, $linkername)) {
			$this->$linkername($object);
		} else {
			$this->_rmLink($object);
		}
		
		$linkername = "UnLink_" . $this->classname;
		if( method_exists($object, $linkername)) {
			$object->$linkername($this);
		} else {
			$object->_rmLink($this);
		}

	}

	function _addLink( $object ) {
		$obj_class = get_class($object);
		//$this_class = get_class($this);
		//$obj_md = $GLOBALS['metadata'][$obj_class]['link'][$this->classname];
		$this_md = $GLOBALS['metadata'][$this->classname]['link'][$obj_class];
		
		switch ($this_md['type']) {
			case '1:1' :
				//$this->setData($link_field, $object->getID());
				//$object->setData($obj_md['field'], $this->getID());
				break;
			case '1:n' :
				// link many objects into 'this' array
				if( !isset($this->data[$this_md["fldname"]]) ) {	// create array if it doesn't already exist
					$this->data[$this_md["fldname"]] = array();
				}
				if( !in_array($object, $this->data[$this_md["fldname"]], true) ) {	//check if object is already in array
					$this->data[$this_md["fldname"]][] = &$object;					// insert if not already present
				}
				// not much else to do, object will copy lnk_fld on its own
				break;
			case 'n:1' :
				// link to single object
				$this->data[$this_md["fldname"]] = &$object;
				// copy lnk_fld if present
				// NOTE - going to copy lnk_fld under all conditions to erase old value if any
				//if( $object->getID() !== null ) {
					$this->setData($this_md["lnk_fld"], $object->getID(), false);
				//}
				break;
			case 'n:m' :
				if( !isset($this->data[$this_md["fldname"]]) ) {	// test if array already exists
					$this->data[$this_md["fldname"]] = array();
				}
				if( !in_array($object, $this->data[$this_md["fldname"]], true) ) {	//check if object is already in array
					$this->data[$this_md["fldname"]][] = &$object;
				}
				break;
			default :
				return false;
		}
		return true;
	}
	
	function _rmlink( $object ) {
		$obj_class = get_class($object);
		$this_md = $GLOBALS['metadata'][$this->classname]['link'][$obj_class];
		
		switch ($this_md['type']) {
			case '1:1' :
				// TODO - add code to remove link - if needed
				break;
			case '1:n' :
				$key = array_search($object, $this->data[$this_md["fldname"]], true);

				if( $key !== false ) {	//check if object is already in array

					array_splice($this->data[$this_md["fldname"]], $key, 1);
				}
				break;
			case 'n:1' :
				$this->data[$this_md["fldname"]] = null;	// detach reference field
				// the line below is temporarily blocked to stop change to dB
				//$this->setData($this_md["lnk_fld"], $object->getID(), false);	// remove ID from lnk_fld, override protection
				break;
			case 'n:m' :
				$key = array_search($object, $this->data[$this_md["fldname"]]);
				if( $key !== false ) {	//check if object is already in array
					unset($this->data[$this_md["fldname"]][$key]);
				}
				break;
			default :
				return false;
		}
		return true;
	}
	
	⋮
}	

You can see as with the Setter/Getter, the generic functions 'Link()' and 'Unlink()' can call specific linkers if they exist or the lower level '_addLink()' and '_rmlink()' giving default behavior.

The linking functions were split into high and low level functions for two reasons:

 1) by having the high level linker call an object specific linker a foreign object can invoke an object's specific linker without even knowing it exists,

and

 2) an object's specific linker can benefit by calling the low level linker while executing additional code before or after the low level linker. This way a programmer can "tweak" the linking process without having to reimplement it.

e.g. When adding/deleting an Inventory Transaction object (line item) to a Sales Order, you may want to invoke a Subtotal recalculation after the un/linking process.



Back to Top
Top

© 2012 and beyond Lawrence L Hovind - All Rights Reserved