An MVC Framework
An MVC Framework

The Model Class


Cascading Setters and Getters

Below is a screenshot from an actual site I developed. It's your typical Sales/Inventory Control system. Shown is the Sales Order form. As you can see this form has several fields. Of particular note is the 'Items' fieldset which can have a variable number of lines or items.

Here's a closeup of the 'Items' section:

In a typical web form the fields would have names like customer_name, item0_name, item1_name, etc.

<form id="salesorder">
	⋮
	<input name="customer_name" value="" />
	⋮
	<input name="item0_name" value="" />
	<input name="item1_name" value="" />
	⋮
</form>

And then you would have a monster of a script translating all those field names into object data fields.

<?php
	⋮
	$Customer->setData("name", $_POST["customer_name"]);
	⋮
	$Items = $SalesOrder->getData("Items");
	if( is_array($Items) ) {
		if( array_key_exists("item0_name", $_POST) ) {
			if( array_key_exists(0, $Items) ) {
				if( is_object($Items[0]) && (get_class($Items[0]) == "InvTrans") ) {
					$Items[0]->setData("name", $_POST["item0_name"]);
				} else {
					⋮
				}
			} else {
				⋮
			}
		}
		if( array_key_exists("item1_name", $_POST) ) {
			if( array_key_exists(1, $Items) ) {
				if( is_object($Items[1]) && (get_class($Items[1]) == "InvTrans") ) {
					$Items[1]->setData("name", $_POST["item1_name"]);
				} else {
					⋮
				}
			} else {
				⋮
			}
		}
		⋮
	}
	⋮
?>

Every time you needed to make a change to the form you also have to make a change to the processing script making sure to translate the additional data. Fertile ground for errors.

What I imagined was the whole form being processed by a single setDataFromRequest($_POST). This way I could make one function call and the entire hierarchy of the Sales Order object would be populated from form data. If, during development, changes were needed to the data in the form, only the form needed editing, not any action/controller script. No more translating field names and less opportunity for errors to creep in.

Here's how it works:

As already shown, the Model class can link objects. So this SalesOrder object (the primary object behind the form) would be linked to a Customer object as well as an array of Inventory Transaction (Line Item) objects.

To set the name of the customer would look something like this:

$SalesOrder->getData("Customer")->setData("name", "Acme Supplies");

But that doesn't translate well from a html form field name. But what if the field name told us the path of the linked objects? It would look something like this:

from the html form:
<input name="Customer:name" value="Acme Supplies" />
to the Setter:
$SalesOrder->setData("Customer:name", "Acme Supplies");

This would tell the Setter that the field to set is "name" of the "Customer" object.

A more complex field name occurs for the line transaction name. It looks like this:

from the html form:
<input name="Items(0):Item:name" value="Special Widget" />
to the Setter:
$SalesOrder->setData("Items(0):Item:name", "Special Widget");
		// which is the same as:
		// $SalesOrder->getData("Items")[0]->getData("Item")->setData("name", "Special Widget");
	

Here we want the setter to realize that the field name is a multipart field name, break down the field name into its parts and call setData() for each part in a cascade. This yields a pleasantly powerful and convenient Setter/Getter ... a Cascading Setter/Getter.

And here's how it's done:

The Setter/Getter parses the incoming field name for array notation (the parenthesis) and scope resolution (the colon). Upon finding them, it separates the field name into parts. The first part is the actual objects field name, the second is an array index, if any, and the third part is the remainder of the field name. It then calls the indexed object's Setter with the field name being the remainder. The called Setter then performs the same parsing on the new field name drilling down until the actual object and field are finally reached and the value is set. I also added the convenient effect of creating linked objects if they don't already exist, as you will need to put the incoming data someplace and some objects might not have been loaded due to lazyload.


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 for and process multi-part field names
		$fldnames = explode(":", $varname);
		
		if( count($fldnames) > 1 ) {
			// find object referred to and call recursive setData()
			// multi-part field name processing
			$fldname = $fldnames[0];
			
			// check for "()" in fldname - extract index
			$idx = -1;
			$pos = strpos($fldname, "(");
			if( $pos !== false ) {
				$idx = intval(substr($fldname, ($pos+1), (strpos($fldname, ")")-$pos-1) ));
				$fldname = substr($fldname, 0, $pos);
			}
			
			$links = $md["link"];
			// does fldname exist in link[lnk_fld]? If so, then this is the name of a linked object
			if( array_key_exists($fldname, $md["lnk_fld"] ) ) {
				$obj_class = $md["lnk_fld"][$fldname]["object"];
				$fldname = $md["link"][$obj_class]["fldname"];
				$type = $md["link"][$obj_class]["type"];
				switch ($type) {
					case "1:1" :
					case "n:1" :	// many of these to one up link
						if( !isset($this->data[$fldname]) ) {
							// create new object if it didn't already exist
							$this->Link( new $obj_class );
						}
						// set data field in new object
						array_shift($fldnames);
						$nvarname = implode(":", $fldnames);
						return $this->data[$fldname]->setData( $nvarname, $value, $protected );
						break;
					case "1:n" :	// one of these to an array of objects
						if( !isset($this->data[$fldname][$idx]) ) {
							// create new object if it didn't already exist
							$new_obj = new $obj_class;
							// add new object at specific array index
							$this->data[$fldname][$idx] = $new_obj;
							$this->Link( $new_obj );
						}
						// set data field in new linked object
						array_shift($fldnames);
						$nvarname = implode(":", $fldnames);
						return $this->data[$fldname][$idx]->setData( $nvarname, $value, $protected );
						break;
					case "n:m" :	// table of cross links
						// TODO - its a bit more involved ;-)
						break;
				}
			}
		} else {
			$fldname = $varname;
		}
		
		// check that the $varname matches one of the data object's field names
		// ignore $fldname if no match
		if( array_key_exists( $fldname, $md["field"] ) ) {
		
			// check this is NOT a protected field or primary key
			if( $protected && ((substr($fldname, 0, 1) == "_") || ($fldname == $md["pkey"]["fieldname"])) ) {
				// this field is protected and will not be set
				error_log("Not setting protected field: " . $fldname);
				return false;
			}
			
			$this->data[$fldname] = $value;
			$this->changed[$fldname] = $value;
			$this->clean = false;
			
		}
		
	}
	
	⋮
}	

It would make more sense to use square brackets instead of parenthesis for the array notation, but PHP preprocesses the brackets and creates total havoc with the incoming data field names. If you're not using PHP, this would be the way to go. If there is a way to turn this off in PHP, will someone please let me know. Using 'php://input' only solves the problem when not using file uploads. But even still, other server-side languages/systems might not play nice with bracket notation. They are too smart for our good.

I like a period (.) for scope resolution but some browsers translate them into an underscore (_). Double colons (::) is a standard for scope resolution and could easily be used here. But I compromised and used just a single character of the standard scope resolution. It seems obvious enough, but if it creates problems down the road it can be changed.



Back to Top
Top

© 2012 and beyond Lawrence L Hovind - All Rights Reserved