Skip to content

Instantly share code, notes, and snippets.

@Golpha
Last active December 17, 2022 16:48
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Golpha/c64bc7a67db1ed9463f7fec13e1e6f4c to your computer and use it in GitHub Desktop.
Save Golpha/c64bc7a67db1ed9463f7fec13e1e6f4c to your computer and use it in GitHub Desktop.
PHP Object-hacks without Reflections: Injectors and Closure Binding References

PHP Object-hacks without Reflections

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.

Reading and writing private and protected properties using injectors

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.

The Injector class
/**
 * 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

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.

Conclusion

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.

The other side of the medal: What is Best Practice ?

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.

Thanks to

This guide is heavly inspired by Marco "Ocramius" Pivetta's blog-post. Thank you, Ocramius.

About the author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment