Exploring PHP Lazy Objects: A Practical Implementation
PHP Lazy Objects - a brief introduction
One of the new features in PHP 8.4 is Lazy Objects, which are part of the PHP OOP model. It's not often that something new gets introduced, so it's worth taking a closer look to understand how it works. Especially since the community seems a bit confused and unsure about how to make use of Lazy Objects, often dismissing them as niche.
By the way, a big thanks to Baptiste Leduc for your article and for inspiring me to dive deeper into the topic of Lazy Objects in PHP: PHP Object Lazy-Loading is More Than What You Think
I invite you to explore the result of my "play" with Lazy Objects, where I'll demonstrate how they can be used to reduce database queries during user authentication.
But first, a brief introduction:
PHP Lazy Objects
In PHP 8.4, support for two strategies was introduced:
- Ghost Objects - ReflectionClass::newLazyGhost()
- Virtual Proxies - ReflectionClass::newLazyProxy()
Both strategies are very similar in usage, with only minor differences.
Ghost Objects are objects that are initialized in place. They are indistinguishable from the base object. Essentially, we have our object, but its values are not initialized, yet the object is used in the code as usual. Only when an attempt is made to access something from the object is it initialized.
Virtual Proxies are a bit different. As the name suggests, they rely on a proxy that forwards every operation to the base object. In the example below, we'll use this strategy.
In this article, I'll focus on a sample implementation. For more details and examples, refer to the well-documented official guide: PHP manual Lazy Objects
Lazy Ghost vs Lazy Proxy
Below is a comparison of the properties of both strategies:
Feature | Ghost Objects | Virtual Proxies |
---|---|---|
Initialization Strategy | Initialization occurs in place upon first access | Real object is created by the proxy upon first access |
Properties | Properties are initialized in place | Proxy forwards operations to the real object |
Transparency | After initialization, the object is a "normal" object | After initialization, the proxy delegates all operations to the real object |
Use Case | When full control over instantiation and initialization is required | When the object is created by an external system or needs deferred initialization |
Cloning | Initialized before cloning | Cloning includes both the proxy and the real object |
Why, what for, and who needs it?
I mentioned at the beginning that Lazy Objects left the community feeling a bit confused. I've come across various opinions, such as criticisms that the functionality is hidden behind reflection, as well as mixed voices for and against its inclusion.
Another frequent argument is that this is a niche feature, unsuitable for everyday use in projects.
Well… everyone has a point to some extent, but I wouldn’t completely align with any of these claims.
Lazy Objects are undoubtedly a high-level feature that can enable optimizations in containers or tools like Doctrine ORM, where similar lazy strategies are already used. Now, with native support, achieving the same functionality requires less effort - which is fantastic!
I approached the topic from the perspective of daily use of Lazy Objects in projects.
Many ideas came to mind, such as using them globally in tests to reduce the number of queries at the start of each test. Or wrapping communication with external API services. Of course, the implementation isn’t trivial, and the topic itself is complex, which likely explains the community’s initial hesitation.
To address this, I invite you to take a look at my example implementation of Lazy Objects:
My Implementation - reducing database queries during user authentication
Let’s move to practice.
The example will focus on a backend application exposing an API for a frontend application. Authorization is performed using JWT tokens. In this example, I’ll demonstrate how Lazy Objects can help reduce database queries for fetching full user data.
Note! This is an educational example meant to showcase how Lazy Objects work. It is not an implementation that should be used in production!
The example is framework-agnostic. To give you an overview, here’s what the project structure for this example looks like:
Okay, you already know what the structure looks like. Now take a look at the basic dependency, which is the UserEntity:
The entity contains basic user data and a relationship to an address.
Now, look at the repository that fetches the user, e.g., from the database:
This is just an example, so we’re not connecting to a database. I’ve left an echo statement so you can later see when the repository is being used.
Now, the main part and the reason you’re here: the implementation of the authenticator class responsible for processing the JWT token into a user instance.
Take a look at it. Below, I’ve provided a detailed explanation of what’s happening.
Let’s break down what’s happening in this class step by step:
- the class returning a user object (Entity, Model, DTO, or whatever you need),
- using Lazy Proxy, we define that our user’s initialization will be performed by the repository,
- we then set the data we have from the token (like ID and email) to the user proxy. This is crucial because we don’t need to fetch this data from the database,
- we’re creating a Proxy object that doesn’t have all the data but includes essential information like the ID and email. This is important because you don’t always need all the data - sometimes, just the user ID is enough.
Note the use of the setRawValueWithoutLazyInitialization method. This is where we set the ID and email, allowing us to use them without referencing the repository and, as a result, avoiding database queries.
Take a look at the dump of such a "$user" object:
As you can see above, the ID and email properties are initialized, so you can safely use them. Only accessing other properties, like firstName, will “load” the data from the database.
In this situation, we don’t need to query the database, but we can still work with the object as if the query had been executed. Isn’t that great?
Take a look at how this works in practice. We have an example controller where we need an authorized user to perform some operation.
Here’s what happens when you run the above controller:
Let’s take it a step further and say I now need the full user data, including their first name:
Here’s what happened.
The code reached the repository, and at that moment, a database query would have been executed.
The need to use data not included in the JWT token forces the query to the database, while if the data isn’t required, the query isn’t executed.
Summary
I hope this example illustrates how Lazy Objects work in PHP. Of course, this is a proof of concept and an attempt to demonstrate the functionality. I aim to provide perspective and inspire thinking about how to use such solutions in the applications we write. Naturally, only when it makes sense - this is a crucial condition.
I think it’s a fantastic feature, and it’s great that the contributors dedicated their time to adding it to PHP.
So, special thanks go to Arnaud Le Blanc and Nicolas Grekas!