Feature: #110347 - Native lazy objects for Extbase lazy loading
See forge#110347
Description
Extbase now uses native PHP lazy objects
(available since PHP 8.4) to implement lazy loading of relations in domain
models annotated with the
# attribute.
Previously, two dedicated proxy classes were used, which are now deprecated:
\TYPO3\for 1:n and m:n relationsCMS\ Extbase\ Persistence\ Generic\ Lazy Object Storage \TYPO3\for 1:1 and n:1 relationsCMS\ Extbase\ Persistence\ Generic\ Lazy Loading Proxy
The DataMapper now creates the following constructs instead:
- For 1:n and m:n relations, a lazy ghost instance of the regular
\TYPO3\class. The storage fetches its content from the persistence layer on first access.CMS\ Extbase\ Persistence\ Object Storage - For 1:1 and n:1 relations, a lazy proxy instance of the actual target entity class. The related record is fetched on first access, and the proxy then transparently forwards all calls to the mapped entity.
Concept
A native lazy object is a real instance of its class: A lazy loaded
Category
parent is a
Category
, and a lazy loaded
Object is an
Object. The PHP engine tracks the
initialization state and invokes an initializer on first property access. There
is no foreign proxy class anymore that only mimics the API of the real object.
The uid of a lazy 1:1 or n:1 relation is available without database access, since it is known from the parent's database row and set eagerly on the uninitialized proxy. Dirty checking, URI generation and form rendering therefore do not trigger relation loading anymore.
Benefits
instanceofchecks against the target entity class are now true for uninitialized lazy relations.-
Native property types can be used for lazy properties. The union type workaround is no longer needed:
// Before #[Extbase\ORM\Lazy] protected Category|LazyLoadingProxy|null $parent = null; public function getParent(): ?Category { if ($this->parent instanceof LazyLoadingProxy) { $this->parent->_loadRealInstance(); } return $this->parent; } // After #[Extbase\ORM\Lazy] protected ?Category $parent = null; public function getParent(): ?Category { return $this->parent; }Copied! - Type declarations of methods consuming such relations can rely on the actual model class.
- Lazy relations no longer carry references to the
Dataor the parent object as object state.Mapper
Serialization
Serializing Extbase entities and object storages is now well-defined:
- Serializing a lazy relation initializes it first and serializes the actual entity data. Previously, the internal proxy state (raw field value, property name and a back reference to the whole parent object graph, in older TYPO3 versions even the DataMapper state) was serialized.
Objectnow implementsStorage __andserialize () __. The contained objects are stored as a plain list and the internalunserialize () spl_based bookkeeping is rebuilt onobject_ hash () unserialize. Calling() detach,() containsor() offseton an unserialized storage now works correctly. Previously, the storage silently kept stale object hashes of the original process and those methods did not work on the restored storage.Get () - Payloads serialized with older TYPO3 versions can still be unserialized.
Impact
Lazy loading works transparently for domain models using the
# attribute. No migration is required: Existing
models keep working, the attribute remains the single way to declare a lazy
relation.
Extension authors may optionally simplify their models:
- Union type declarations like
Categorycan be reduced to the actual entity type, for example|Lazy Loading Proxy |null ?Category. - Calls to
LazyandLoading Proxy->_ load Real Instance () instanceof Lazychecks in getters can be removed, the property value is an instance of the target class in all cases.Loading Proxy -
Whether a lazy object has been initialized can be determined with native PHP reflection:
$isUninitialized = new \ReflectionClass(ObjectStorage::class) ->isUninitializedLazyObject($storage);Copied!
Behavioral notes:
- Calling
counton an uninitialized lazy object storage now fully initializes the storage with one query. Previously, a dedicated() COUNTquery was executed without initializing the storage. - If a lazy 1:1 or n:1 relation points to a record that cannot be resolved
anymore (for example a deleted or hidden record), the parent property is
reset to
nullon first access, as before. Code holding the proxy instance itself encounters an empty entity instance instead of the previousnullreturn value of_load.Real Instance ()