Extbase model - extending AbstractEntity
All classes of the domain model should inherit from the class
\TYPO3\
.
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 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;
}
}
Warning
Extbase does not call the constructor when thawing objects. Therefore you cannot set default values or initialize properties in the constructor. This includes properties that are defined via constructor parameter promotion. See also Default values for model properties.
Table of content
Subpages
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:
final class Tag extends AbstractValueObject
{
public string $name = '';
public int $priority = 0;
}
Or public getters:
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;
}
}
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.
Note
Making model's properties
private
does not work in Extbase models: The parent
classes need to access the models properties directly. If your model must
not be extended you can mark it as
final
and thereby prevent
other developers from extending your model.
It is also possible to have getters for properties that are not persisted and get created on the fly:
class Info extends AbstractEntity
{
protected string $name = '';
protected string $bodytext = '';
public function getCombinedString(): string
{
return $this->name . ': ' . $this->bodytext;
}
}
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:
Does not display "combinedString":
<f:debug>{post.info}</f:debug>
But it is there:
<f:debug>{post.info.combinedString}</f:debug>
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:
<?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
}
$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.
Note
Typed properties are strongly encouraged in all new TYPO3 extensions.
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
__
) does not workconstruct (public string $title = '') - 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:
class Blog extends AbstractEntity
{
protected string $title = '';
protected ?\DateTime $modified = null;
}
Good: Use initializeObject()
for setup
If a property needs special setup (for example, using new Object
),
you can put that logic into a method called initialize
. Extbase
calls this method automatically after loading the object:
<?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();
}
}
Avoid: Constructor property promotion
This will not work when the object comes from the database:
public function __construct(protected string $title = '') {}
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 initialize
method.
TCA default values
If the TCA configuration of a field defines a
default value, that value is applied after
initialize
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:
<?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;
}
This is especially useful for lazy-loaded relations where the property type is
Child
.
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.
<?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;
}
In this case,
string
is the primary type.
int
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
results in the
primary type
string
which is nullable.
null
also results in the primary type
string
. In fact,
null
means that all other types are nullable.
null
boils down to
?string
or
?int
.
Secondly,
Lazy
is never detected as a primary type because it
is just a proxy and not the actual target type, once loaded.
<?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;
}
Extbase supports this and detects
Child
as the primary type,
although
Lazy
is the first item in the list. However, it is
recommended to place the actual type first, for consistency reasons:
Child
.
A final word on
\TYPO3\
:
it is a subclass of
\TYPO3\
,
therefore the following code works and has always worked:
<?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;
}
Extending existing models
It is possible, with some limitations, to extend existing Extbase models in another extension. See also Tutorial: Extending an Extbase model.