Virtual getters and setters using __call()
Getters and setters are good practice in object oriented programming. It hides the internal structure of the object and allows you to perform additional processing of values during retrieval or storage. It can also be a pain to write all your getters and setters, especially if you're classes have a lot of accessible properties.
PHP's __call()
magic method
While not appropriate for every project, and not without it's own limitations, it's fairly easy to create "virtual" getters and setters using PHP's __call()
magic method. This method allows us to intercept calls to inaccessible properties or methods on our objects. We'll need to define this method and provide the necessary code to make it handle like a getter and setter.
class Foo
{
public function __call ( $name, $arguments )
{
}
}
With the __call()
method defined, we can start adding logic to it. The first piece of logic will be to test that the called method starts with get
or set
. The called function is available as $name
.
public function __call ( $name, $arguments )
{
// get the first three letters of $name
$prefix = strtolower(substr($name, 0, 3));
if ($prefix === 'get')
{
// getter
}
elseif ($prefix === 'set')
{
// setter
}
}
We capture the first three characters of $name
and convert all the characters to lower case. Methods are case insensitive, so we need to handle values like getFoo
, getfoo
or GetFoo
equally.
A working implementation
Implementing the getter logic is usually pretty straightforward. We simply need to return the value of the requested property. The name of the property is available as the first element in $arguments
. In the case of the setter, we also need to pull the value to assign to the property from $arguments
.
public function __call ( $name, $arguments )
{
// get the first three letters of $name
$prefix = strtolower(substr($name, 0, 3));
// get the remaining letters of $name
$property = substr($name, 3);
if ($prefix === 'get')
{
// getter
return $this->$property;
}
elseif ($prefix === 'set')
{
// setter
$value = $arguments[0];
$this->$property = $value;
}
}
At this point, we have a functional implementation of virtual getters and setters.
class Bar extends Foo
{
protected $fruit = 'apple';
}
$obj = new Bar();
echo $obj->getfruit(); // outputs "apple"
$obj->setfruit('orange');
echo $obj->getfruit(); // outputs "orange"
While it may be functional, this implementation has some design flaws that need to be corrected before it could be used in a production environment.
Sanity checking
Building on our example above, what would happen if we had called $obj->getFruit()
. Our __call()
method would have blindly attempted to return the value of the non-existent property Bar::$Fruit
, raising a PHP Notice message Undefined property: Bar::$Fruit ...
. To avoid this, we need to test that the property we're getting or setting actually exists first. We can use property_exists()
for this purpose.
public function __call ( $name, $arguments )
{
...
// get the remaining letters of $name
$property = substr($name, 3);
// make sure the property exists, else return
if (!property_exists($this, $property))
{
return;
}
if ($prefix === 'get')
...
}
Any get or set calls for non-existent properties will now return void. It's a good start, but it doesn't solve the problem of calling $obj->getFruit()
, which you would expect to return apple
, but which currently returns void
. That's because we're simply using the 4th to the last characters in $name
, which in this case leaves us with Fruit
. As property names are case sensitive, Bar::$Fruit
doesn't exist. We need to accomodate the fact that variables and properties tend to start with a lowercase letter.
...
// get the remaining letters of $name
$firstLetter = strtolower(substr($name, 3, 1));
$property = $firstLetter.substr($name, 4);
...
We capture the fourth letter in $name
separately, convert it to lowercase, then append the remaining letters to it to get our property name. Now, calling $obj->getFruit()
gives us the property name fruit
, which will return the expected value. Calling $obj->getFooBar()
, will give us the property name fooBar
, which is what most would expect.
Controlling access to properties
Currently, our virtual getters and setters have no concept of privacy. Consider the following example.
class Bar extends Foo
{
protected $sensitiveInternalData = 'top secret';
}
$obj = new Bar();
echo $obj->getSensitiveInternalData(); // outputs "top secret"
Developers limit access to object properties for good reasons. Exposing all these internal protected
and private
properties to the rest of the application can be troublesome and perhaps even dangerous. Because our virtual getters and setters operate within the scope of the object, they have unrestricted access to this potentially sensitive data. We need to build in a privacy system that we can use to restrict access to such properties.
Our system of privacy will be prepending underscores to property names that we don't want to be accessible outside the object. This means Bar::$fruit
isn't private while Bar::$_fruit
is and Bar::$__fruit
is as well.
public function __call ( $name, $arguments )
{
...
// make sure the property exists, else return
if (!property_exists($this, $property))
{
return;
}
// make sure the property is accessible outside the object
if (substr($property, 0, 1) === '_'))
{
return;
}
if ($prefix === 'get')
...
}
Let's modify our example above to work with the new code we've just added.
class Bar extends Foo
{
protected $_sesitiveInternalData = 'top secret';
}
$obj = new Bar();
echo $obj->getSensitiveInternalData(); // outputs ""
The call to $obj->getSensitiveInternalData()
returns void now.
Our completed implementation
class Foo
{
public function __call ( $name, $arguments )
{
// get the first three letters of $name
$prefix = strtolower(substr($name, 0, 3));
// get the remaining letters of $name
$firstLetter = strtolower(substr($name, 3, 1));
$property = $firstLetter.substr($name, 4);
// make sure the property exists, else return
if (!property_exists($this, $property))
{
return;
}
// make sure the property is accessible outside the object
if (substr($property, 0, 1) === '_'))
{
return;
}
if ($prefix === 'get')
{
// getter
return $this->$property;
}
elseif ($prefix === 'set')
{
// setter
$value = $arguments[0];
$this->$property = $value;
}
}
}
Overriding the default behaviour
In cases where you want to override the default behaviour of a getter or setter, it's easy enough to implement your own.
class Foo
{
public function __call ( $name, $arguments )
{
...
}
// handle bar differently
public function getBar ()
{
// do custom getter stuff
}
}
PHP only calls __call()
when no existing method can be called. In this case, calling $obj->getBar()
uses the defined getter, not the virtual getter.
Trading performance for convenience
It's certainly convenient to use virtual getters and setters but as with anything, there's a tradeoff. The performance of virtual getters and setters can be at least 5 times slower than standard getter and setters. It may not be an issue for small applications, but large applications or those with high-traffic would probably want to avoid this approach.
Performance may also be affected in situations where you need to test for the existence of a getter or setter method. Calling method_exists('Foo', 'getBar')
will return false if using a virtual getter, requiring additional code and CPU cycles to workaround.
Comments