php[architect] logo

Want to check out an issue? Sign up to receive a special offer.

Using PHP 8.4’s Lazy Objects

Posted by on January 27, 2025

One of the more interesting features added to PHP 8.4 is the ability to create Lazy Objects. Lazy Objects allow us to defer the initialization of an object until one of its properties is accessed. This may not be a game-changer for the average CRUD application, but it will allow us to make some amazing performance-related changes to our code.

In this article, we’ll discuss what Lazy Objects are and how to use them in PHP 8.4.

What Are Lazy Objects/Lazy Initialization

Lazy initialization of an object is a tactic where the initialization of an object is delayed until the first time it’s needed and not immediately like normally. This can be helpful in several cases but my favorite example is when we have a large collection of objects that have a potentially expensive initialization but we might not always use all of the objects in the collection.

For example, we have a class that connects to a service so it can retrieve some value. We might be looping through a collection of this class looking for the “correct” object based on a value. If we have 100 items in our array, we’re going to perform 100 slow operations only to have a large number of them discarded without actually using them.

class InternetItems
{
    public function __construct()
    {
        echo "Making connection to external service", PHP_EOL;
    }

    public function getValueFromService(): int
    {
        // this "mocks" the call to the service
        return rand(1, 100);
    }
}


$items = InternetItems::get();
foreach ($items as $item) {
    if ($item->getValueFromService() == 50) {
        return $item;
    }
}

What we want to do is delay the connection to the external service until it’s necessary because it can be very slow.

Before 8.4 it was possible to implement this feature in userland code by having a skinny or no __construct() method and then adding logic to our class to initialize the connection before it’s used for the first time.

// No
class UserlandLazy
{
    public function initConnection():void
    {
        echo "Making connection to external service", PHP_EOL;
    }

    public function getValueFromService(): int
    {
        $this->initConnection();
        return rand(1, 100);
    }
}

This works okay, but we have to be careful to make sure to always initialize the values before we use them, and there’s a potential performance hit because we might call initConnection() repeatedly. It’s workable, but it’s far from being a great solution. Thankfully, if we’re using PHP 8.4 or higher, we have a much better option.

What are PHP Lazy Objects?

Lazy Objects came in the PHP 8.4 release, which was released in November 2024.

PHP 8.4 provides support for two lazy object strategies: Ghost Objects and Virtual Proxies.

In both strategies, the lazy object is attached to an initializer that is called automatically when its state is observed or modified for the first time. Lazy ghost objects are indistinguishable from non-lazy ones and can be used without knowing they are lazy, allowing them to be passed to and used by code that is unaware of laziness. Lazy proxies are similarly transparent, except for the fact that the proxy and its real instance have different identities. We’ll show an example of this a little later.

The actual change to the language adds new methods to the ReflectionClass and ReflectionProperty classes that will enable us to create an object that doesn’t call its __construct() method until a property is accessed. This includes non-existent properties, which shouldn’t be a problem but might come back to bite you.

Ghost Example

To use this functionality, we first need to define our class the way we want to work with it. This includes having a constructor that makes the connection to the external service and then loads the value of interest. To make the example simple, I’m going to fake it and just generate a random number.

class PhpLazy
{
    public string $valueFromService;
    public function __construct()
    {
        echo "Making connection to external service", PHP_EOL;
        $this->valueFromService = rand(1, 100);
    }
}

Next, we’re going to use the ReflectionClass to define how to initialize the lazy ghost object by using the newLazyGhost() function.

$reflectionClass = new ReflectionClass(PhpLazy::class);
$phpLazyObject = $reflectionClass->newLazyGhost(function (PhpLazy $object) {
    // Initialize object here
    $object->__construct();
});

Then we can use it like any other instance of a class..

// This doesn't trigger initialization
var_dump($phpLazyObject);
var_dump(get_class($phpLazyObject));

// Accessing property triggers initialization
var_dump($phpLazyObject->valueFromService);
var_dump($phpLazyObject);

The output of this is as follows (from the new “Run code” feature on php.net):

lazy ghost object(PhpLazy)#3 (0) {
  ["valueFromService"]=>
  uninitialized(string)
}
string(7) "PhpLazy"
Making connection to external service
string(2) "86"
object(PhpLazy)#3 (1) {
  ["valueFromService"]=>
  string(2) "45"
}

Note that before we trigger the initialization var_dump($phpLazyObject) shows “lazy ghost object” as the type, but afterward, it shows PhpLazy as the type.

Proxy Example

Creating a proxy is very similar to our ghost example, except we use the newLazyProxy() function and return a new instance of the class.

$reflectionClass = new ReflectionClass(PhpLazy::class);
$phpLazyObject = $reflectionClass->newLazyProxy(function (PhpLazy $object) {
    // Create and return the real instance
    return new PhpLazy();
});

Then we can use it like before.

var_dump($phpLazyObject);
var_dump(get_class($phpLazyObject));

// Triggers initialization
var_dump($phpLazyObject->valueFromService);
var_dump($phpLazyObject);

And get the output.

lazy proxy object(PhpLazy)#3 (0) {
  ["valueFromService"]=>
  uninitialized(string)
}
string(7) "PhpLazy"
Making connection to external service
string(1) "4"
lazy proxy object(PhpLazy)#3 (1) {
  ["instance"]=>
  object(PhpLazy)#4 (1) {
    ["valueFromService"]=>
    string(1) "4"
  }
}

Note that before we trigger the initialization var_dump($phpLazyObject) shows “lazy proxy object” as the type, but after we trigger the initialization, it still shows “lazy proxy object” as the type, but with the PhpLazy class in the instance property. This is where we need to be aware of the difference between the ghosts and proxy versions.

What You Need To Know

  1. PHP 8.4 added support to create lazy object
  2. Ghost Lazy Objects are indistinguishable from non-lazy objects
  3. Lazy proxies have a different identity from the non-lazy objects

Tags: , ,
 

Leave a comment

Use the form below to leave a comment: