Extbase model - extending AbstractEntity

All classes of the domain model should inherit from the class \TYPO3\CMS\Extbase\DomainObject\AbstractEntity .

An entity is an object fundamentally defined not by its attributes, but by a thread of continuity and identity, for example, a person or a blog post.

Objects stored in the database are usually entities as they can be identified by the uid and are persisted, therefore have continuity.

In the TYPO3 backend models are displayed as Database records.

Example:

Class T3docs\BlogExample\Domain\Model\Comment
class Comment extends AbstractEntity
{
    protected string $author = '';

    protected string $content = '';

    public function getAuthor(): string
    {
        return $this->author;
    }

    public function setAuthor(string $author): void
    {
        $this->author = $author;
    }

    public function getContent(): string
    {
        return $this->content;
    }

    public function setContent(string $content): void
    {
        $this->content = $content;
    }
}
Copied!

Persistence: Connecting the model to the database

It is possible to define models that are not persisted to the database. However in the most common use cases you want to save your model to the database and load it from there. See Persistence.

Properties of an Extbase model

The properties of a model can be defined either as public class properties:

Class T3docs\BlogExample\Domain\Model\Tag
final class Tag extends AbstractValueObject
{
    public string $name = '';

    public int $priority = 0;
}
Copied!

Or public getters:

Class T3docs\BlogExample\Domain\Model\Info
class Info extends AbstractEntity
{
    protected string $name = '';

    protected string $bodytext = '';

    public function getName(): string
    {
        return $this->name;
    }

    public function getBodytext(): string
    {
        return $this->bodytext;
    }

    public function setBodytext(string $bodytext): void
    {
        $this->bodytext = $bodytext;
    }
}
Copied!

A public getter takes precedence over a public property. Getters have the advantage that you can make the properties themselves protected and decide which ones should be mutable.

It is also possible to have getters for properties that are not persisted and get created on the fly:

Class T3docs\BlogExample\Domain\Model\Info
class Info extends AbstractEntity
{
    protected string $name = '';

    protected string $bodytext = '';

    public function getCombinedString(): string
    {
        return $this->name . ': ' . $this->bodytext;
    }
}
Copied!

One disadvantage of using additional getters is that properties that are only defined as getters do not get displayed in the debug output in Fluid, they do however get displayed when explicitly called:

Debugging different kind of properties
Does not display "combinedString":
<f:debug>{post.info}</f:debug>

But it is there:
<f:debug>{post.info.combinedString}</f:debug>
Copied!

Typed vs. untyped properties in Extbase models

In Extbase, you can define model properties using either PHP native type declarations or traditional @var annotations. Typed properties are preferred, untyped properties are still supported for backward compatibility.

The example below demonstrates a basic model with both a typed and an untyped property:

EXT:my_extension/Classes/Domain/Model/Blog.php
<?php

declare(strict_types=1);

namespace Vendor\Extension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Blog extends AbstractEntity
{
    /**
     * Typed property (preferred)
     *
     * @var string
     */
    protected string $title = '';

    /**
     * Untyped property (legacy-compatible)
     *
     * @var bool
     */
    protected $published = false;

    // Getters and Setters
}
Copied!
  • $title is a typed property, using PHP’s type declaration. This is the recommended approach as it enforces type safety and improves code readability.
  • $published is an untyped property, defined only with a docblock. This remains valid and is often used in older codebases.

For persisted properties (those stored in the database), ensure that the property type matches the corresponding TCA Field type to avoid data mapping errors.

Nullable and Union types are also supported.

Default values for model properties

When Extbase loads an object from the database, it does not call the constructor.

This is explained in more detail in the section thawing objects of Extbase models.

This means:

  • Property promotion in the constructor (for example __construct(public string $title = '')) does not work
  • Properties must be initialized in a different way to avoid runtime errors

Good: Set default values directly

You can assign default values when defining the property. This works for simple types such as strings, integers, booleans or nullable properties:

EXT:my_extension/Classes/Domain/Model/Blog.php
class Blog extends AbstractEntity
{
    protected string $title = '';
    protected ?\DateTime $modified = null;
}
Copied!

Good: Use initializeObject() for setup

If a property needs special setup (for example, using new ObjectStorage()), you can put that logic into a method called initializeObject(). Extbase calls this method automatically after loading the object:

EXT:my_extension/Classes/Domain/Model/Blog.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Blog extends AbstractEntity
{
    protected ObjectStorage $posts;

    public function __construct()
    {
        $this->initializeObject();
    }

    public function initializeObject(): void
    {
        $this->posts = new ObjectStorage();
    }
}
Copied!

Avoid: Constructor property promotion

This will not work when the object comes from the database:

public function __construct(protected string $title = '') {}
Copied!

Since the constructor is never called during hydration, such properties remain uninitialized and can cause errors like:

Error: Typed property MyVendorMyExtensionDomainModelBlog::$title must not be accessed before initialization

To prevent this, always initialize properties either where they are defined or inside the initializeObject() method.

TCA default values

If the TCA configuration of a field defines a default value, that value is applied after initializeObject() has been called, and before data from the database is mapped to the object.

Union types of Extbase model properties

New in version 12.3

Previously, whenever a union type was needed, union type declarations led Extbase not detecting any type at all, resulting in the property not being mapped. However, union types could be resolved via docblocks. Since TYPO3 v12.3 native PHP union types can be used.

Union types can be used in properties of an entity, for example:

EXT:my_extension/Classes/Domain/Model/Entity.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model\MyEntity;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

class Entity extends AbstractEntity
{
    private ChildEntity|LazyLoadingProxy $property;
}
Copied!

This is especially useful for lazy-loaded relations where the property type is ChildEntity|\TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy.

There is something important to understand about how Extbase detects union types when it comes to property mapping, that means when a database row is mapped onto an object. In this case, Extbase needs to know the desired target type - no union, no intersection, just one type. In order to achieve this, Extbase uses the first declared type as a so-called primary type.

EXT:my_extension/Classes/Domain/Model/Entity.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model\MyEntity;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Entity extends AbstractEntity
{
    private string|int $property;
}
Copied!

In this case, string is the primary type. int|string would result in int as primary type.

There is one important thing to note and one exception to this rule. First of all, null is not considered a type. null|string results in the primary type string which is nullable. null|string|int also results in the primary type string. In fact, null means that all other types are nullable. null|string|int boils down to ?string or ?int.

Secondly, LazyLoadingProxy is never detected as a primary type because it is just a proxy and not the actual target type, once loaded.

EXT:my_extension/Classes/Domain/Model/Entity.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model\MyEntity;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;

class Entity extends AbstractEntity
{
    private LazyLoadingProxy|ChildEntity $property;
}
Copied!

Extbase supports this and detects ChildEntity as the primary type, although LazyLoadingProxy is the first item in the list. However, it is recommended to place the actual type first, for consistency reasons: ChildEntity|LazyLoadingProxy.

A final word on \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage : it is a subclass of \TYPO3\CMS\Extbase\Persistence\ObjectStorage , therefore the following code works and has always worked:

EXT:my_extension/Classes/Domain/Model/Entity.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model\MyEntity;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Entity extends AbstractEntity
{
    /**
     * @var ObjectStorage<ChildEntity>
     * @TYPO3\CMS\Extbase\Annotation\ORM\Lazy
     */
    private ObjectStorage $property;
}
Copied!

Extending existing models

It is possible, with some limitations, to extend existing Extbase models in another extension. See also Tutorial: Extending an Extbase model.