---
title: "Feature: #110347 - Native lazy objects for Extbase lazy loading"
manual: "TYPO3 Core Changelog"
version: "main"
permalink: "https://docs.typo3.org/permalink/changelog:feature-110347-1785518009"
source: "Changelog/15.0/Feature-110347-NativeLazyObjectsForExtbaseLazyLoading.rst"
modified: "2026-09-15T10:46:36+00:00"
---

# Feature: #110347 - Native lazy objects for Extbase lazy loading

See [forge#110347](https://forge.typo3.org/issues/110347)

## Description

Extbase now uses [native PHP lazy objects](https://www.php.net/manual/en/language.oop5.lazy-objects.php)
(available since PHP 8.4) to implement lazy loading of relations in domain
models annotated with the `#[Extbase\ORM\Lazy]` attribute.

Previously, two dedicated proxy classes were used, which are now deprecated:

-   `\TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage` for 1:n and
    m:n relations
-   `\TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy` for 1:1 and
    n:1 relations

The DataMapper now creates the following constructs instead:

-   For 1:n and m:n relations, a **lazy ghost** instance of the regular
    `\TYPO3\CMS\Extbase\Persistence\ObjectStorage` class. The storage
    fetches its content from the persistence layer on first access.
-   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
`ObjectStorage` *is* an `ObjectStorage`. 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

-   `instanceof` checks 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:

    ```php
    // 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;
    }
    ```
-   Type declarations of methods consuming such relations can rely on the
    actual model class.
-   Lazy relations no longer carry references to the
    `DataMapper` or the parent object as object state.

### 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.
-   `ObjectStorage` now implements `__serialize()` and
    `__unserialize()`. The contained objects are stored as a plain list
    and the internal `spl_object_hash()` based bookkeeping is rebuilt on
    `unserialize()`. Calling `detach()`, `contains()` or
    `offsetGet()` on 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.
-   Payloads serialized with older TYPO3 versions can still be unserialized.

## Impact

Lazy loading works transparently for domain models using the
`#[Extbase\ORM\Lazy]` 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 `Category|LazyLoadingProxy|null` can be
    reduced to the actual entity type, for example `?Category`.
-   Calls to `LazyLoadingProxy->_loadRealInstance()` and
    `instanceof LazyLoadingProxy` checks in getters can be removed, the
    property value is an instance of the target class in all cases.
-   Whether a lazy object has been initialized can be determined with native
    PHP reflection:

    ```php
    $isUninitialized = new \ReflectionClass(ObjectStorage::class)
        ->isUninitializedLazyObject($storage);
    ```

Behavioral notes:

-   Calling `count()` on an uninitialized lazy object storage now fully
    initializes the storage with one query. Previously, a dedicated
    `COUNT` query 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 `null` on first access, as before. Code holding the proxy
    instance itself encounters an empty entity instance instead of the
    previous `null` return value of `_loadRealInstance()`.
