October 26, 2007
By: Dave Goerlich in Code
As the lead systems architect here at Designing Interactive, I enjoy reviewing new code patterns to see if there are areas in our codebase in which they could be implemented. Recently I spent some time digging into Martin Fowler’s Active Record pattern. While a direct implementation of this exceptional code pattern doesn’t fit within our Sandstone Application Framework, I was inspired to review how we have implemented our business entity classes to see if we could implement some of the more fundamental concepts of the Active Record code pattern.
When building enterprise level software with multi-developer teams, enforcing quality coding standards is critical. Our standards cover everything from architecture, naming conventions, interfaces and database design to version control processes.
Our standards forbid the use of public fields as properties within classes:
All class properties must be implemented through public getter and setter functions using a protected field for data storage:
1 2 3 4 5 6 7 8 9 10 11 | protected $_firstName; public function getFirstName() { return $this->_firstName; } public function setFirstName($Value) { $this->_firstName = $Value; } |
While we can easily call these public functions as class methods:
In order for the above code to actually operate as a property:
We must implement the magic getter and magic setter functions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public function __get($Name) { $getter='get'.$Name; if(method_exists($this,$getter)) { $returnValue = $this->$getter(); } else { throw new InvalidPropertyException( "No Readable Property: $Name", get_class($this), $Name); } return $returnValue; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public function __set($Name, $Value) { $setter='set'.$Name; if(method_exists($this,$setter)) { $this->$setter($Value); } else if(method_exists($this,'get'.$Name)) { throw new InvalidPropertyException( "Property $Name is read only!", get_class($this), $Name); } else { throw new InvalidPropertyException( "No Writeable Property: $Name", get_class($this), $Name); } } |
Obviously this shows that we have extended the Exception object to create an InvalidPropertyException within the Sandstone Application Framework.
This structure supports read-only properties through simply implementing just the public getter function. Also computed or combined properties are supported as well:
1 2 3 4 5 6 7 | public function getFullName() { $returnValue = $this->_firstName . " " . $this->_lastName; return $returnValue; } |
Our standards for database design have allowed us to develop some more specific standards for how our business entity classes are developed. In reviewing these classes, I was not surprised to find that the code for the class constructors and the methods for loading and saving data to the database all followed identical formats. In the case of the constructors, more often than not, the code was identical from class to class.
One of our biggest goals when refactoring code is the DRY principle. Here I was faced with a significant amount of repeated code – how could this be corrected? A standard solution to this type of scenario is to employ the superclass extraction refactoring method. What was needed was an EntityBase class, from which all business entity classes could inherit.
Our EntityBase class centralized the common constructor functionality. In order to centralize the basic plumbing of the Load and Save methods, we stepped back and took a more OO view of the fundamental parts of a class definition itself – specifically properties. We realized that just as metadata in a database describes the structure and functionality of the data, we can identify “metaproperties” which describe the structure and functionality of the actual properties. Some of the metaproperties we identified were:
Our solution implemented an architecture where individual properties of a business entity class were represented as objects themselves.
Under this new architecture, properties are no longer implemented as fields and functions, but are now defined through an array of PropertyClass objects. An overridden protected method in each EntityBase child (called from the EntityBase constructor) performs the setup of this array of properties. This array is keyed by the property name for easy lookup. In order to make this functional, we had to override our magic getter and magic setter functions in the EntityBase class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public function __get($Name) { if (array_key_exists(strtolower($Name), $this->_properties)) { $targetProperty = $this->_properties[strtolower($Name)]; $returnValue = $targetProperty->PropertyValue; } else { throw new InvalidPropertyException( "No Readable Property: $Name", get_class($this), $Name); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public function __set($Name, $Value) { if (array_key_exists(strtolower($Name), $this->_properties)) { $targetProperty = $this->_properties[strtolower($Name)]; if ($targetProperty->IsReadOnly == false) { $targetProperty->PropertyValue = $Value; } else { throw new InvalidPropertyException( "Property $Name is read only!", get_class($this), $Name); } } else { throw new InvalidPropertyException( "No Writeable Property: $Name", get_class($this), $Name); } } |
Within our PropertyClass, the setPropertyValue($Value) function enforces rules set by the values of the other metaproperties. For example, it enforces data type rules (i.e. cannot set a string value to a numeric property) and does not allow a required property to be set to null.
Support for computed or combined properties can be retained through a small addition to the magic getter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public function __get($Name) { $getter='get'.$Name; if(method_exists($this,$getter)) { $returnValue = $this->$getter(); } elseif (array_key_exists(strtolower($Name), $this->_properties)) { $targetProperty = $this->_properties[strtolower($Name)]; $returnValue = $targetProperty->PropertyValue; } else { throw new InvalidPropertyException( "No Readable Property: $Name", get_class($this), $Name); } } |
This refactoring through superclass extraction has done an excellent job of DRYing up our business entity classes. The new architecture allows our developers to create new business entity classes in a quarter of the time. It has also allowed us to easily add functionality (such as tagging) to all business entity classes in a single step.
This new architecture did bring with it one significant challenge. How do we implement scope-based security? We still needed to manipulate the values of properties from within the class itself without having the public access rules enforced. We might need to set a required property to null, or the ability to change the value of a read-only property. Under traditional design, the compiler would handle this security validation and generate parse errors if some external source attempted to access the protected fields. Now, the question I needed to answer was, “How can I tell at run time if the call is coming from this exact instance of the class or not?” I realized the answer lies in the call stack!
I had worked with the debug_backtrace() function previously while building a custom debug and error page for the Sandstone Application Framework. According to the documentation, the “object” element of the returned array provided a reference to the calling object (if any). That’s exactly what I needed to perform this scope check at run time. I did find that in order for that object reference to be set, Zend Optimizer 3.3.0 or higher is required. Once the Zend Optimizer install on our server was updated, everything worked as advertised.
Our PropertyClass also exposes a Value property in addition to the PropertyValue. Unlike the PropertyValue property which enforces all public access rules, the Value property provides that unrestricted modification of the actual value we require.
To implement this scope level security, we first added the following protected method to the EntityBase class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | final protected function IsInternalCall() { $callStack = debug_backtrace(); //The call context we are interested in //will be index 2 in the array. // 0 = this function // 1 = internal function call to this test // 2 = context in question $context = $callStack[2]; if ($context['object'] === $this) { $returnValue = true; } else { $returnValue = false; } return $returnValue; } |
It’s important to note that on line 13 we are using the ===, to test if the object reference in the array is the same instance of the current object.
With this method in place, we made a few changes to the magic getter and magic setter implementations. Our standard naming conventions state that all protected field names are prefixed with an underbar:
Therefore we can use a leading underbar in the name as the indicator that the calling function is requesting local scope level access to the property. All we have to do now when such access is requested, is validate this is an internal call:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | public function __get($Name) { $getter='get'.$Name; if(method_exists($this,$getter)) { $returnValue = $this->$getter(); } elseif (array_key_exists(strtolower($Name), $this->_properties)) { $targetProperty = $this->_properties[strtolower($Name)]; $returnValue = $targetProperty->PropertyValue; } elseif(substr($Name, 0, 1) == "_" && $this->IsInternalCall()) { //Only allow this for internal calls //Determine the associated property name $propertyName = strtolower( substr($Name, 1, strlen($Name) - 1)); //Does it exist? if (array_key_exists($propertyName, $this->_properties)) { //Return it's value $targetProperty = $this->_properties[$propertyName]; $returnValue = $targetProperty->Value; } else { throw new InvalidPropertyException( "No Readable Property: $Name", get_class($this), $Name); } } else { throw new InvalidPropertyException( "No Readable Property: $Name", get_class($this), $Name); } return $returnValue; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | public function __set($Name, $Value) { $setter='set'.$Name; if(method_exists($this,$setter)) { $this->$setter($Value); } elseif (array_key_exists(strtolower($Name), $this->_properties)) { $targetProperty = $this->_properties[strtolower($Name)]; if ($targetProperty->IsReadOnly == false) { $targetProperty->PropertyValue = $Value; } else { throw new InvalidPropertyException( "Property $Name is read only!", get_class($this), $Name); } } elseif(substr($Name, 0, 1) == "_" && $this->IsInternalCall()) { //Only allow this for internal calls //Determine the associated property name $propertyName = strtolower( substr($Name, 1, strlen($Name) - 1)); //Does it exist? if (array_key_exists($propertyName, $this->_properties)) { //Set it's value $targetProperty = $this->_properties[$propertyName]; $targetProperty->Value = $Value; } else { throw new InvalidPropertyException( "No Writeable Property: $Name", get_class($this), $Name); } } else { throw new InvalidPropertyException( "No Writeable Property: $Name", get_class($this), $Name); } } |
This code implementation not only provides the scope level security we require, but does so while preserving the interface of a traditional protected field. This reduced the time required to convert all previously existing business entity classes and maintains a standard naming convention across both business entity classes and other functional classes.
Since making this major change to the Sandstone Application Framework architecture, we have completed a custom application project for a client. I am happy to report that this new structure, and a couple RAD tools built in-house specifically to support the new architecture, allowed us to build and deploy the application in record time.
Comments
Danny Sedor » November 2, 2007
This is a perfect example of the SuperClass Extraction refactoring method and all developers should take note. This is an amazing example of maintaining scope-level security while avoiding the need to proverbially re-invent the wheel. It will defintiely make life (work) easier moving forward.