August 12, 2011
Making magic objects with PHP
By DineEngine

PHP 5 provides us with the ability to introduce magic into our code via its set of Magic Methods. These methods get called automatically (depending on the situation) and allow the developer to add extra capabilities to their scripts. I am going to focus on a set that I found to be useful in creating what I like to call a “Magic Object.” Magento developers have enjoyed the functionality, which I am going to simplify, for some time now, with the $object->get*() and $object->set*() model calls, all based off of the Varien_Object.

The basic premise of this Magic Object is that you can create objects that don’t require exposing member variables to the public, but you also won’t need to write getters and setters for each of them. What you will learn is how to create an object that can have method calls to retrieve class variables, for example getting $object->foo with $object->getFoo() instead. I will focus on the method __call(), and not __get() and __set() as you might expect. The reason being that these two methods allow you to perform the same end function as __call(), but lack the object oriented feel that __call() provides us. But enough with the explanation, here’s some code (sorry for the lack of color coding).

/**
 * A magic object!
 * @author Jason
 *
 */
class MObject {
	/** Cache for our underscore'd names */
	private $_underscore = array();

	function __call($key, $args) {
		if(strlen($key) > 3) {
			//Getter
			if(substr($key, 0, 3) == 'get') {
				$var = $this->_to_underscore(substr($key, 3));
				if($this->_has_variable($var)) {
					return $this->$var;
				}
				else{
					throw new Exception('Method not found');
				}
			}
			//setter
			elseif(substr($key, 0, 3) == 'set' && count($args) > 0){
				$var = $this->_to_underscore(substr($key, 3));
				if($this->_has_variable($var)) {
					$this->$var = $args[0];
				}
				else{
					throw new Exception('Method not found');
				}
			}
		}
	}

	/**
	 * Determines whether or not a variable exists in the object
	 * @param mixed $variable
	 * @return true|false
	 */
	protected function _has_variable($variable) {
		$vars = get_object_vars($this);
		foreach($vars as $var => $val) {
			if($var == $variable) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Converts a camelcase variable name to lowercase + underscores
	 * @param string $variable
	 */
	protected function _to_underscore($variable) {
		//check the cache
		if(array_key_exists($variable, $this->_underscore)) {
			return $this->_underscore[$variable];
		}

		$variable[0] = strtolower($variable[0]);
		$func = create_function('$c', 'return "_" . strtolower($c[1]);');
		$name = preg_replace_callback('/([A-Z])/', $func, $variable);
		$this->_underscore[$variable] = $name;
		return $name;
	}

}

And now for an explanation. What this code is doing is looking for any calls to $object->>getSomeVariable() or $object->setSomeVariable(), and attempting to locate that variable to either get or set it. Yes, obvious — but how it does it is where the magic comes in! Any class that extends this object can have protected (not private, visibility issues) variables that don’t need to be exposed to the world, but still allow other objects access to them in a controlled fashion. This methodology also promoted uniformity in coding, where you are forcing yourself or another developer to always use the same calling pattern to access data from an object. Developer A can’t use $object->getFoo() while Developer B opts for $object->foo, potentially leading to buggy, or at the very least confusing, code.

The way we are accomplishing this is in the __call() method. First, it is checking to see if the method called is either a get or set command. If so, then it will enumerate through the member variables of the object, checking for an “uncamelized” match on the variable name. What that means is getSomeVariable() will look for the variable $some_variable. The Magic Object has given you free getters and setters for all of your member variables!

I hear you already, “why wouldn’t I just use $object->foo to access the variable?” As mentioned earlier, this makes the code more uniform, forcing developers to adopt method calls rather than direct access to the variable. You also get the benefit of being able to create your own getter or setter for the variable if you need it!

Consider that you have a member variable $created_date, which is based off of a time() call. If you were to use $object->created_date to access this, you will be forcing yourself to format the date in every call. Or maybe you’re clever and went ahead and created a getFormattedDate() method to take care of that. Fine and well, but you are still forcing other developers, and yourself, to remember another method name. However, if you are using a Magic Object, you can override the getDate() method to accept an argument indicating whether or not to format the date, with a default value for the argument as a convenience. The developer is happier, and you have less code to maintain — a win for everyone!

The other side of the Magic Object is setting variables. Since you and your developers are used to setting variables with setVariable() now, you want to keep that practice in place. If a variable requires some sort of validation prior to being set, you can simply override setVariable() to perform the checks necessary, and then allow or disallow the operation.

But wait, there’s more! Now that you have a nice, unified set of calls to access data, you can continue to create getters and setters for items such as database entries while maintaining the pattern of get*() and set*(). When the developer doesn’t have to remember if something has to be called with $object->getFoo() or $object->foo, time is saved, and (hopefully) bugs are reduced.

And since I didn’t have a good place to put this, one last piece of information: if you don’t want to give access to a variable in your subclass, simply make it private and it won’t be visible to __call(), and as such won’t be available to the outside world.

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

Trusted by top brands.

Get Started with DineEngine.

Contact Us Now Find Out How Chepri Can Help Your Team. Ask Us More About Making magic objects with PHP.

(800) 338-8102

733-C Lakeview Plaza Blvd. Worthington, OH 43085.