---
title: 'Lazy Objects in PHP 8.4 with Doctrine Step-by-Step Code Example'
source: 'https://youtube.com/watch?v=HflZlZ-5zho'
video_id: 'HflZlZ-5zho'
date: 2026-08-03
duration_sec: 1231
---

# Lazy Objects in PHP 8.4 with Doctrine Step-by-Step Code Example

> Source: [Lazy Objects in PHP 8.4 with Doctrine Step-by-Step Code Example](https://youtube.com/watch?v=HflZlZ-5zho)

## Summary

This video provides an introductory overview of PHP 8.4's lazy objects feature, using Doctrine ORM as a practical example. It explains the concept of lazy loading, how the new reflection-based API works, and demonstrates its integration with Doctrine's proxy system, including a detailed analysis of when initialization is triggered.

### Key Points

- **Introduction to Lazy Objects RFC** [00:02] — PHP 8.4 introduces a lazy objects RFC, a technical feature with many moving parts. The video offers a surface-level introduction using Doctrine ORM as an example.
- **What is Lazy Loading?** [00:14] — Lazy loading defers the constructor of an object until the first use. If the object is never used, its constructor is not called, and in Doctrine ORM, it is not loaded from the database, saving resources.
- **RFC Authors and Background** [01:44] — The RFC was proposed by Ano Leblanc and Nicolas Grias. Nicolas has worked on lazy loading and proxies in Symfony, and Doctrine has used lazy proxies since version 2, recently switching to Symfony's implementation.
- **Reflection API and Lazy Ghost** [02:56] — The RFC adds new functions to the Reflection class. Doctrine uses the 'newLazyGhost' method, which creates objects that defer constructor loading until the first property access, using an initializer callback.
- **Initialization Triggers** [04:37] — Lazy loading is triggered by reading/writing a property, checking if a property is set, unsetting, or using reflection methods like getValue/setValue. Cloning the object also triggers initialization.
- **Doctrine Example Setup** [05:37] — A small project with a Post entity (ID, message, author) uses Doctrine 3.4 development branch to demonstrate the integration. The 'getReference' method creates a lazy reference.
- **Profiling Lazy Loading** [06:53] — Using a profiler, the video shows that calling getMessage triggers lazy loading (database query), while getID does not because the ID is set without initialization using 'setRawValueWithoutLazyInitialization'.
- **Proxy Factory Implementation** [09:08] — The proxy factory checks if native lazy objects are enabled, then calls ReflectionClass::newLazyGhost with an initializer that calls 'loadById' to populate the object from the database.
- **Property Accessor and Raw Value** [12:41] — Doctrine's property accessor uses 'setRawValueWithoutLazyInitialization' to set known identifiers without triggering lazy loading, which is why getID doesn't cause initialization.
- **Author ID Without Loading** [16:42] — Printing the author's ID does not trigger a database load because the identifier is known and set without initialization, demonstrating the efficiency of lazy objects.
- **Loading Author Name** [18:16] — Accessing the author's name triggers lazy loading, resulting in a database query for the user entity, as shown in the profiler.

### Conclusion

Lazy objects in PHP 8.4 offer a powerful way to defer object initialization, improving performance by avoiding unnecessary database loads. Doctrine's integration showcases how this feature can be used to optimize resource usage in real-world applications.

## Transcript

about PHP8.4's lazy objects RFC. This is a very technical feature that uh has a lot of moving parts. So on
in this video I'm going to make an surface level introduction to it. Uh using doctrine RM as an example. We are going to look into how example. We are going to look into how doctrine RM uses lazy objects and uh how
it benefits from it going forward. So what is lazy loading? Lazy loading is essentially deferring the constructor of an object until the first
constructor of an object until the first time the object is being used. So it means that the constructor of the object is not being called until you actually start using the object. If you don't use the object then the
constructor will not be called and in case of doctrine ORM the object will not be loaded from the database at all. However the object is already present in the code in the object graph. So it can be passed around
um to the different parts in the system. It can be passed to a template. It can It can be passed to a template. It can be passed to a a method uh performing logic. And as long as you don't access any of the properties of the lazy
object, it will not be loaded into memory. And uh therefore it saves a lot of resources. Let's dive into this uh RFC a little. So
the RFC has been proposed by Ano Leblanc and Nicolas Grias and uh especially Nicholas has been working on the concept of lazy loading and lazy proxies in
of lazy loading and lazy proxies in Symphfony as well. So um doctrine had its own implementation for lazy loading proxies uh since version lazy loading proxies uh since version two um contributed in
two um contributed in 2012 and uh recently switched to the Symfony implementation um using the Symfony var exporter component which Nicolas wrote. So, Nicholas is very
knowledgeable about proxy objects and knows a lot of the edge cases and he's uh the domain expert that contributed to uh this RFC and Ano is the engine level um expert uh who is also working for the PHP foundation uh on on PHP as a
language. So, this RFC was accepted and added to PHP 8.4 for and we are going to look how it works um based on the doctrine doctrine example. Let's uh get a little deeper to
uh some code examples to understand how it's working. So this RC adds uh new functions that you can call on um the reflection class. So it's a reflection level feature meta
programming and in case of doctrine we are using the new lazy ghost method and are using the new lazy ghost method and lazy ghosts are objects that are lazy ghosts are objects that are um the real object and deferred
constructor loading until the first um property is accessed. um property is accessed. This is done using the initializer um call back. So you pass the initializer and the initializer is
responsible for loading the object into its state and calling the not going into in this video because it's uh against um uh very technical RC.
it has its own in-depth things that we need to look at and maybe the topic of a different video later. So the way you are using it is
later. So the way you are using it is you have an initializer for um your you have an initializer for um your class. you call uh you instantiate a new reflection class and then you call reflector new
lazy ghost and then you have an instance of my class that's not initializ it yet of my class that's not initializ it yet and initialization is triggered by a few and initialization is triggered by a few different things let's look at this RFC
different things let's look at this RFC into the initialization triggers when is an object triggered uh and when will lazy loading be triggered Uh this section lists a few of the cases. So reading or writing a property,
testing if a property is set or unsetting it, calling reflection unsetting it, calling reflection property, get value, set value or the PHP 8.4 four equivalents for property hooks. Get raw value um and set raw
value calling reflection object get properties get property cloning the properties get property cloning the object and uh this all causes initialization. What is initialization? The co the initialization call back is
The co the initialization call back is called. So here what we see here this called. So here what we see here this call back is called and uh afterwards call back is called and uh afterwards the object is marked as
example that I've prepared before to see how this actually works. how this actually works. I have prepared a little project and it I have prepared a little project and it has a post entity in
doctrine and we are using the doctrine 3.4 u um development branch because the integration with this lazy loading uh RFC uh is not uh released
yet. So um the post entity has just an ID and a message and a reference to an author. So it's very straightforward. Um and we are using a small script as an example here to see how this works. So
in doctrine uh the method get reference on the entity manager creates um an a lazy reference of the post object.
So the way this works is uh we have the get ID method and the get message and now we're trying to see how lazy loading works under the hood. Let's look at this um using a profiler so that we can see the
the um runtime behavior of uh the proxy RFC and lazy objects feature. And uh And uh I'm a huge fan of trying to understand
code not only by reading the code in the IDE but also running it um and in a profiler understanding the uh the the trace flow. So let's look
uh the the trace flow. So let's look at calls to the function uh two um calls to the two um methods on the post object. So we had get id and we had get object. So we had get id and we had get message and um the output of the
script prints the ID and prints the message here. So how does lazy loading come into play for the proxy? So that's play for the proxy? So that's um easy to see in this case because
um easy to see in this case because uh get ID is called once and uh is very fast. uh get message however takes unreasonably long for unreasonably long for um uh a simple getter. And it's uh easy
um uh a simple getter. And it's uh easy to see why because uh as you can see here uh get message has a child function closure proxy factory get proxy and you can see below it calls the database at
the end and um also connects to the database. So post get message the rendering of the message is triggering the lazy loading. And now let's try to find out how that works and why the lazy losing wasn't
works and why the lazy losing wasn't triggered by calling the get ID method. So uh we use proxy factory get proxy line 173 as an entry point to uh
understand this. And going back to the IDE, we are And going back to the IDE, we are searching for proxy searching for proxy factory and trying to understand that
173 is what we are looking at. So we can see here uh entity manager get configuration is native lazy objects enabled is checked. So this is a new enabled is checked. So this is a new functionality and I have called this on
the doctrine config object enable native lazy objects with um doctrine 3.4 we are going to release the old and the new proxies side by side and you can opt into them. Uh the reason for this is this feature requires 8.4 obviously not
this feature requires 8.4 obviously not uh all users of the ORM3 are using it. Um but also since it's a completely different implementation, we want users to opt into it and test it on their applications before moving. However, our
goal is that we are removing the old way of generating proxies in doctrine 4 when we increase the requirement to PHP 8.4 for and our the benefit we will
gain from this is that we can get rid of the code generation proxy approach and also all the engine level hacks that we are using to implement um proxy are using to implement um proxy generation at the moment. So lazy native
lazy objects are enabled. We are looking at the proxy factory. What does it do? It calls reflection class new lazy ghost. That is reflection class new lazy ghost. That is what we saw in the RFC. Uh it creates an
initializer here and the initializer essentially does only one method call. essentially does only one method call. It calls entity persist load by ID using an identifier and an object. The object is
passed to the initializer and the identifier is bound to this closure as a use variable. So we are keeping the identifier for this proxy uh inside the
identifier for this proxy uh inside the initializer and when the initialization is called we use this to um call the load by ID method on the entity persistor the entity persistor is a doctrine API low-level API that loads an
object from the database and when the second object uh is when the second argument is passed passed as an object, it will use that object as a reference it will use that object as a reference and populate the properties of
need uh for this feature to work and nothing more. So it's really just using new lazy ghost uh this initializer. We already have this load by ID method available um in the doctrine API. we didn't need
to implement anything new and this essentially worked out of the box without many changes. So this still this doesn't answer the question why the method called post get ID didn't trigger lazy
called post get ID didn't trigger lazy loading. So to remind you um we have post get message triggering it. Post get ID didn't trigger it. And uh if we look ID didn't trigger it. And uh if we look back at the code, post get id is called
first. So it should have triggered it uh given that it was uh printing the value already. So what is this proxy magic at
place there? So all magic is there to be understood. That's actually very simple. Uh doctrine has an API called property accessoruses. uh property accesses API
we had to add um to support property hooks and doctrine and it also enabled us to support proxy objects and we will see for um why this is the case um in a
few seconds. Let's go down the rabbit hole hole here. Property accessor set value.
So let's go into that. It's an interface and we have a few implementations. So what we could do now is look at the profiler which implementation is
used raw value property accessor set value and type no default property accessor set value. Those two are called. So let's look at both of
accessor is um a delegate. So it calls another property accessor in set value and this is what's happening in our case. So let's look at the second
implementation raw value property accessor. And the set value method of this one is what we are looking at. We will we see set raw value without lazy initialization. So what does that mean?
initialization. So what does that mean? Uh going back to the RFC, we talked about initialization triggers and the lazy objects RFC comes triggers and the lazy objects RFC comes with a few features to work around lazy
with a few features to work around lazy loading. uh that means we can set a property to a value already without triggering initialization and then if we read from this
property the value is already present and initialization doesn't have to happen. So this is what's happening here. Uh we see here the following special cases do not trigger initialization of a lazy object. Set raw
value without lazy initialization. So let's look again in the profiler if this method is being called. Set raw value without lazy
called. Set raw value without lazy initialization is being called. So initialization is being called. So here the lazy initialization is um circumvented. This value is set on the property on the on the entity already.
property on the on the entity already. And um this is why calling post get ID does not trigger lazy loading. Uh let's go back to
loading. Uh let's go back to the code example to try to understand it the code example to try to understand it again. We load a post object from the database. Get reference. No, we don't load it from the database. We create a
lazy object for this post entity with ID 1 inside the proxy factory. Get proxy is called for the post class with the identifier. It's
already a known information. The identifier for an instance of an entity. This known information is then used to populate the property. And this
used to populate the property. And this is why it doesn't trigger when get ID is called. Uh it's getting called when get message is called because this is not available yet. Initialization is triggered. The entity is loaded. Both
get message and also the author are being populated at that point. So let's play around with the proxy uh script a little bit more. Let's add echo for the
author ID. and again run this with the profiler. So here we see post ID message hello world one author ID 1. So this worked. Let's take a look at what's
happening here. In the overview we can see again um a doctrine load for post uh but we don't see a doctrine load for the author. Let's look in the call graph
for all calls to the user object user get ID is being called it doesn't have get ID is being called it doesn't have children um child calls. So um again here since the uh identifier is known while constructing the proxy a set raw
value without initialization is used and reading this value back from the object does not trigger lazy loading and we can print the ID of the user without loading
the object from the database. So let's take it a little um uh one step further. Let's print the name of the author and
then the name of the author. And now if we look at the profile we see there is a we look at the profile we see there is a doctrine call to a user to load the user entity. And we see also a query on the user
user table. And uh looking again at the user uh get name has a runtime that is quite large for uh just a getter. And looking at this we see again here a call to the
proxy factory. Um and we see the proxy factory is called from two different locations. post get message. We already saw that before. And now from user get name. So both of these
loading and um load this information into memory. So thank you very much for watching this video. I hope you liked um
watching this video. I hope you liked um my introduction into lazy loading uh and my introduction into lazy loading uh and the lazy loading feature in PHP 8.4. If you want to consume similar content like this, uh please um uh subscribe to this
this, uh please um uh subscribe to this channel and uh continue watching uh and looking out for my content that goes deep into PHP performance. Uh in next version of this um series, I will look into the lazy proxy equivalent of the
RFC and explain what you can use that for. And I hope you got a good insight for. And I hope you got a good insight into lazy objects and how doctrine works and maybe also got some ideas how you can use this yourself in in your own
code to improve performance by simulating the availability of objects without uh loading them at the time of construction. instead deferring the
loading of the object and deferring the constructor of the object to the latest constructor of the object to the latest possible point where you need the values uh that are assigned to the properties there. Thank you very much for
listening. Uh I hope to see you back soon.
