Skip to content

Instantly share code, notes, and snippets.

@alcaeus
Created July 25, 2016 15:49
Show Gist options
  • Save alcaeus/fcc12672d7e46faa23e6c35516802e7b to your computer and use it in GitHub Desktop.
Save alcaeus/fcc12672d7e46faa23e6c35516802e7b to your computer and use it in GitHub Desktop.

Partially initialized proxy objects

This is a PoC for partial proxy objects in Doctrine. Properties can be dynamically filled without initializing the proxy object. Once a property is modified or an uninitialized property is read, the entire proxy object will be initialized with data from the database.

How does it work?

The idea is to unset all properties in the constructor. This is currently done for public properties to allow lazy-loading them. This principle is applied to all properties. Once a property is modified, its value is set using reflection to avoid creating the property in a public scope.

Open issues

Currently, it is possible to either read or write a single private or public property from outside the objects scope. This is due to the fact how magic methods work. There would need to be logic in place to prevent this.

class ProxiedProperty
{
/** @var string */
private $name;
/** @var \ReflectionProperty */
private $reflectionProperty;
/** @var object */
private $owner;
public function __construct($owner, string $name, \ReflectionProperty $reflectionProperty)
{
$this->owner = $owner;
$this->name = $name;
$this->reflectionProperty = $reflectionProperty;
$this->reflectionProperty->setAccessible(true);
}
public function setValue($value)
{
$this->reflectionProperty->setValue($this->owner, $value);
}
public function getValue()
{
return $this->reflectionProperty->getValue($this->owner);
}
}
class Proxy extends Document
{
/** @var ProxiedProperty[] */
private $properties;
/** @var \Closure */
private $initializer;
/** @var bool */
private $initialized = false;
/** @var array */
private $internalProperties = ['properties', 'initialized', 'initializer', 'internalProperties'];
public function __construct($initializer)
{
$this->initializer = $initializer;
$this->initializeProperties();
}
public function initializePartialProperties($properties)
{
foreach ($properties as $name => $value) {
if (! isset($this->properties[$name])) {
throw new \Exception('Not found');
}
$this->properties[$name]->setValue($value);
}
}
private function initialize()
{
$initializer = $this->initializer;
$this->initializer = null;
$this->initialized = true;
$initializer->__invoke($this);
}
private function initializeProperties()
{
$reflectionClass = new \ReflectionClass(get_parent_class($this));
$privateProperties = [];
foreach ($reflectionClass->getProperties() as $reflectionProperty) {
$propertyName = $reflectionProperty->getName();
$this->properties[$propertyName] = new ProxiedProperty($this, $propertyName, $reflectionProperty);
if ($reflectionProperty->isPrivate()) {
$privateProperties[] = $propertyName;
} else {
unset($this->$propertyName);
}
}
if ($privateProperties !== []) {
$closure = function (Document $document) use ($privateProperties) {
foreach ($privateProperties as $property) {
unset($document->$property);
}
};
$unsetProperties = Closure::bind($closure, $this, get_parent_class($this));
$unsetProperties($this);
}
}
public function __get($name)
{
if (isset($this->properties[$name])) {
if (! $this->initialized) {
echo "__get({$name}): Initialize proxy\n";
$this->initializer && $this->initialize();
}
return $this->properties[$name]->getValue();
}
trigger_error(sprintf('Undefined property: %s::$%s', __CLASS__, $name), E_USER_NOTICE);
}
public function __isset($name)
{
if (isset($this->properties[$name])) {
if (! $this->initialized) {
echo "__isset({$name}): Initialize proxy\n";
$this->initializer && $this->initialize();
}
return isset($this->name);
}
return false;
}
public function __set($name, $value)
{
if (isset($this->properties[$name])) {
if (! $this->initialized) {
echo "__set({$name}): Initialize proxy\n";
$this->initializer && $this->initialize();
}
$this->properties[$name]->setValue($value);
return;
} elseif (in_array($name, $this->internalProperties)) {
trigger_error(sprintf('Undefined property: %s::$%s', __CLASS__, $name), E_USER_NOTICE);
}
$this->$name = $value;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment