Properties
Concepts
(TODO: discuss disadvantages of direct access to $obj->var and how properties address this.)
With this in mind, let's define some terminology:
- Accessor - the method used to "get" a property's value
- Mutator - the method used to "set" a property's value
The accessor name often consists of the property name but is prefixed by the verb "get" or (for boolean types only) the verb "is". For example, the property myVar would have the accessor method getMyVar(). Or the boolean property switchedOn would have an accessor named isSwitchedOn. Likewise, a mutator is instead prefixed with the verb "set". For example, the property myVar would have the mutator method setMyVar().
Properties also come in three different flavors:
- read-write - has both an accessor and a mutator
- read-only - has only an accessor but no mutator
- write-only - has only a mutator but no accessor
The Property Object
Phorce implements properties through the class PropertyObject within the namespace phorce\common\property. In order to use Phorce's property support in your code, you need to extend PropertyObject, which overrides the property resolution operator through PHP's magic __get() and __set() methods. When a property is accessed using this operator, PropertyObject will attempt to resolve the name of the accessor or mutator using the naming rules described above.
The following code demonstrates a simple use of PropertyObject.
<?php
use phorce\common\property\PropertyObject;
class MyObject extends PropertyObject
{
public function getMessage()
{
return "Hello World";
}
}
$obj = new MyObject();
echo $obj->message, "\n"; // invokes the getMessage() method
// these will throw PropertyAccessException
echo $obj->message = "blah"; // no "set" method
echo $obj->foobar, "\n"; // no "get" method
?>
In the example above, the message property is read-only because there is only an accessor defined and no mutator. And while this serves well as a first example, in practice you will rarely need to have an accessor return a hard-coded value. More often, you will need both an accessor and a mutator to interact with private member variables. The next example illustrates this usage with PropertyObject:
<?php
use phorce\common\property\PropertyObject;
class MyObject extends PropertyObject
{
private $_message;
public function getMessage()
{
return $this->_message;
}
public function setMessage($v)
{
$this->_message = $v;
}
}
$obj = new MyObject();
echo $obj->message = "Hello World"; // now this works, calls setMessage()
echo $obj->message, "\n"; // invokes the getMessage() method
?>
Thus, the private member variable _message is represented by both an accessor and mutator. Access to the variable is done in a completely controlled way and the methods may be altered if additional functionality is needed.
However, one of the primary disadvantages of explicitly writing accessors and mutators is that it is very verbose. Each property must have it's own member variable, accessor method, and mutator method. The coding standard in these examples express this as 11 lines of code for each property. The math is pretty easy here: for a class with only 10 properties that's 110 lines of code! You will watch your classes grow pretty quickly with all this boiler-plate code.
It would be nice if there were a more compact way of expressing a property when all you need is basic setting and getting. Fortunately, there is. The PropertyObject, if it cannot find a public accessor or mutator method that matches the property name, will then look for a member variable matching the property name and prefixed with "p_". A property can then be expressed much more succinctly as follows:
<?php
use phorce\common\property\PropertyObject;
class MyObject extends PropertyObject
{
private $p_message;
}
$obj = new MyObject();
$obj->message = "Hello World"; // sets the value of the message property
echo $obj->message, "\n"; // retrieves the value of the message property
echo $obj->message2, "\n"; // throws PropertyAccessException
?>
This reduces the declaration of a property down to one line of code. The "p_" prefix also gives the property the read-write flavor. For other flavors we can use the "pRO_" and "pWO_" prefixes, as follows:
<?php
use phorce\common\property\PropertyObject;
class MyObject extends PropertyObject
{
private $p_message;
private $pRO_value1 = "can't touch this";
private $pWO_value2;
}
$obj = new MyObject();
$obj->message = "Hello World"; // sets the value of the message property
echo $obj->message, "\n"; // retrieves the value of the message property
echo $obj->value1, "\n"; // this works ok
echo $obj->value2, "\n"; // throws an exception
$obj->value1 = "blah"; // throws an exception
$obj->value2 = 123; // this works ok
?>
Now, we only need to provide accessor or mutator methods where we want to override the default behavior such methods would normally provide. For example, say we want to validate that the message is always a string value. We can write a setMessage() mutator which updates the property value manually.
<?php
use phorce\common\property\PropertyObject;
class MyObject extends PropertyObject
{
private $p_message;
private $pRO_value1 = "can't touch this";
private $pWO_value2;
/**
* Override the mutator for property $obj->message
*/
public function setMessage($v)
{
// validate the parameter
if (! is_string($v))
throw new Exception("Message must be a string");
$this->p_message = $v;
}
}
$obj = new MyObject();
$obj->message = 12345; // not a string - throws an exception
$obj->message = "Hello World"; // this will pass validation
echo $obj->message, "\n"; // this still works as before
?>
We can also add additional accessor methods to our object to derive information from this property. These accessors will appear to behave as read-only properties to users of the class as no mutator is defined.
<?php
use phorce\common\property\PropertyObject;
class MyObject extends PropertyObject
{
private $p_message;
private $pRO_value1 = "can't touch this";
private $pWO_value2;
/**
* Override the mutator for property $obj->message
*/
public function setMessage($v)
{
// validate the parameter
if (! is_string($v))
throw new Exception("Message must be a string");
$this->p_message = $v;
}
/**
* Defines an accessor for property $obj->monospace
*/
public function getMonospace()
{
return "<pre>" . $this->p_message . "</pre>";
}
/**
* Defines an accessor for property $obj->empty
*/
public function isEmpty()
{
return empty($this->p_message);
}
}
$obj = new MyObject();
$obj->message = "Hello World";
if (! $obj->empty)
echo $obj->monospace, "\n";
?>
Criticism & Recommendations
It should be noted that this type of approach is not without its share of critics. The first and most common criticism is that by abstracting a method call in this way, you are making it less clear to the user of your class what exactly is going on. For example, it is quite clear with $obj->getValue() that you are calling a method. However, it's not clear when you do $obj->value if you are calling a method or retrieving the value in a variable. A user unfamiliar with this syntax may think that they are merely accessing a member variable and not realizing that there may be, for example, database access going on under the hood and all the I/O overhead that comes with it. (as a side note, this is a major reason why this type of syntax is presently absent from Java)
For their part, the critics are partly correct. It is not possible anymore from code alone to tell exactly what is happing with $obj->value because the property resolution operator is being overloaded. That is why it is important to provide sufficient documentation to users of your class so it is clear what exactly is happening and how your code should be used. And although this syntax makes it quite possible, it is strongly advised that you avoid the temptation to use properties to access external data resources directly (e.g. databases). For anything with a high degree of I/O, processing, or otherwise significant performance overhead please use a method.