Sometimes it is needed to manipulate private and protected properties of instances. The common way to do this is to manipule the visibility of the targeted properties using class reflection, but there is a more memory and performance efficient way to accomplish this task.
At first injectors are not standardized nor described along design patterns, i choose Injectors as the name for this mechanic because of their major usage. The explicit technical name could be: IO-Adapters for invisible properties. Injectors do use closure binding with a explicit class scope. So, whatever you want to modify does require knowledge about the implementation details of the targeted parameters, as you should have when directly using closure binding.
/**
* Injector Class
*
* @version 1.0
* @package catalyst.extensions.php
*/
class Injector {
/**
* The instance to handle
*
* @var object
*/
private $instance;
/**
* The scope to use
*
* @var object|string
*/
private $scope;
/**
* Class constructor.
*
* @param object $object
* @throws Exception if the type of the first parameter does not match
*/
public function __construct($object)
{
if ( ! is_object($object) ) {
throw new Exception('first constructor parameter must be an object');
}
if ( $object instanceof Closure ) {
throw new Exception('first constructor parameter can not be an closure');
}
$this->instance = $object;
$this->scope = get_class($object);
}
/**
* Scope setter.
*
* @var object|string $scope
*/
public function setScope($scope)
{
$this->scope = $scope
}
/**
* magic getter
*
* @param string $property
* @throws Exception if the property does not exists
* @return mixed
*/
public function __get($property)
{
$accessor = Closure::bind(function($property) {
if ( ! property_exists($this, $property) ) {
throw new Exception(
sprintf('Unknown property: %s', $property)
);
}
return $this->{$property};
}, $this->instance, $this->scope);
try {
return $accessor($property);
}
catch ( Exception $e ) {
throw $e;
}
}
/**
* magic setter.
*
* @param string $property
* @param mixed $newValue
* @throws Exception if the property does not exists
* @return mixed
*/
public function __set($property, $newValue)
{
$accessor = Closure::bind(function($property, $newValue) {
if ( ! property_exists($this, $property) ) {
throw new Exception(
sprintf('Unknown property: %s', $property)
);
}
return $this->{$property} = $newValue;
}, $this->instance, $this->scope);
try {
$accessor($property, $newValue);
}
catch ( Exception $e ) {
throw $e;
}
}
/**
* magic isset.
*
* @param string $property
* @return mixed
*/
public function __isset($property)
{
$accessor = Closure::bind(function($property) {
return property_exists($this, $property);
}, $this->instance, $this->scope);
return $accessor($property);
}
/**
* magic unset.
*
* @param string $property
* @return mixed
*/
public function __unset($property)
{
$accessor = Closure::bind(function($property) {
unset($this->{$property});
}, $this->instance, $this->scope);
$accessor($property);
}
}
To use this Class, create an instance with the an instance and a optional scope class name and access the property to change or read. Thats all:
class Person {
protected $name = 'World';
public function greet()
{
return sprintf('Hello, %s!', $this->name);
}
}
$person = new Person();
$personInjector = new Injector($person);
$personInjector->name = 'John Doe';
echo $person->greet();
Hello, John Doe!
A working live example of this code is available here.
Referenced Closures make use of the implementation details of the Injector Principle above, but they do only allow to read or write properties. Unsetting will have not the same effect. Checking for existance will be impossible. Here we go:
class Person {
protected $name = 'World';
public function greet()
{
return sprintf('Hello, %s!', $this->name);
}
}
$person = new Person();
$accessor = Closure::bind(function & (Person $person) {
return $person->name;
}, null, $person);
$referencedPerson = & $accessor($person);
$referencedPerson = 'John Doe!';
echo $person->greet();
Hello, John Doe!
A working live example of this code is available here.
Due to the possible full-set of features, Injectors are the solution to go. Referenced Closures should be used when performance is the highest priority. Both methods will do the job for modifing or reading non-public properties.
Hacking objects is in general an anti-pattern. It does violate the contracted borders of a class. If you ever come into the situation where hacking objects may be a way to go, don't do it outside of a class. Use referenced Closures inside of a method applied to the class to handle with help of inheritance. Features of a class should be contracted along methods.
Whereas Injectors do this with the ability to use one implementation for possibly all objects except closures, they do violate the contracted class borders as well. Its in a lot of cases better to do a new implementation of a given class with the needed features instead of hacking them.
This guide is heavly inspired by Marco "Ocramius" Pivetta's blog-post. Thank you, Ocramius.
This guide has been authored by Matthias "Golpha Nihylum" Kaschubowski, a PHP evanglist and one of the Administrators of the international PHP Facebook Group from Germany.