<?php
// This is the class that we want to make lazy.
// Note that this is an opaque class from the PoV of the proxy-generation logic:
// its implementation details are unknown.
class A
{
protected array $data = [];
public function __construct()
{
$this->data = ['foo' => 123];
}
public function __get($n) { return $this->data[$n] ?? null; } // line 15
public function __set($n, $v) { $this->data[$n] = $v; }
public function __isset($n) { return isset($this->data[$n]); }
public function __unset($n) { unset($this->data[$n]); }
}
// This is a simplified proxy class that would typically be generated automatically
// from the signature of the proxied class. The way this works is by unsetting all
// declared properties at instantiation time, then using the magic methods to lazily
// initialize them on demand. This is a very simple implementation that does not handle
// all edge cases, but it should be enough to illustrate the issue. The logic in each
// magic method is very repetitive: we checks if we're trying to access the uninitialized
// property. If yes, we initialize it, if not, we call the parent method.
class LazyA extends A
{
private const STATUS_UNINITIALIZED = 0;
private const STATUS_INITIALIZING = 1;
private const STATUS_INITIALIZED = 2;
private $lazyStatus = self::STATUS_UNINITIALIZED;
public function __construct()
{
unset($this->data);
}
public function __get($n)
{
if (self::STATUS_INITIALIZED === $this->lazyStatus || 'data' !== $n) {
if ('data' === $n && (new ReflectionProperty($this, 'data'))->isInitialized($this)) {
return $this->data;
}
return parent::__get($n);
}
$this->initialize();
return $this->data;
}
public function __set($n, $v)
{
if (self::STATUS_INITIALIZED === $this->lazyStatus || 'data' !== $n) {
parent::__set($n, $v);
}
$this->initialize();
$this->data = $v;
}
public function __isset($n)
{
if (self::STATUS_INITIALIZED === $this->lazyStatus || 'data' !== $n) {
return parent::__isset($n);
}
$this->initialize();
return isset($this->data);
}
public function __unset($n)
{
if (self::STATUS_INITIALIZED === $this->lazyStatus || 'data' !== $n) {
parent::__unset($n);
}
$this->initialize();
unset($this->data);
}
private function initialize()
{
if (self::STATUS_INITIALIZING === $this->lazyStatus) {
return;
}
$this->lazyStatus = self::STATUS_INITIALIZING;
parent::__construct();
$this->lazyStatus = self::STATUS_INITIALIZED;
}
}
$a = new LazyA();
// Currently, this will trigger a TypeError:
// Cannot assign null to property A::$data of type array on line 15
// With Ilija's patch, this will work as expected and will display int(123)
var_dump($a->foo);
Here you find the average performance (time & memory) of each version. A grayed out version indicates it didn't complete successfully (based on exit-code).