Model

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.

Example:

EXT:blog_example/Classes/Domain/Model/Comment.php
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!

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

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

EXT:blog_example/Classes/Domain/Model/Tag.php
final class Tag extends AbstractValueObject
{
    public string $name = '';

    public int $priority = 0;
}
Copied!

Or public getters:

EXT:blog_example/Classes/Domain/Model/Info.php
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:

EXT:blog_example/Classes/Domain/Model/Info.php
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!

Relations

Extbase supports different types of hierarchical relationships between domain objects. All relationships can be defined unidirectional or multidimensional in the model.

On the side of the relationship that can only have one counterpart, you must decide whether it is possible to have no relationship (allow null) or not.

Nullable relations

There are two ways to allow null for a property in PHP:

Nullable property types have been introduced with PHP 7.1 and can therefore be used in any modern TYPO3 version:

Example for a nullable property
protected ?Person $secondAuthor = null;
Copied!

Union types, that can also be used to allow null, have been introduced with PHP 8.0 and can only been used when the minimal PHP requirement is PHP 8.0.

Example for union type of null and Person
protected Person|null $secondAuthor = null;
Copied!

Both declarations have the same meaning.

1:1-relationship

A blog post can have, in our case, exactly one additional info attached to it. The info always belongs to exactly one blog post. If the blog post gets deleted, the info does get related.

EXT:blog_example/Classes/Domain/Model/Post.php
use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;

class Post extends AbstractEntity
{
    /**
     * 1:1 optional relation
     * @Cascade("remove")
     */
    protected ?Info $additionalInfo;

    public function getAdditionalInfo(): ?Info
    {
        return $this->additionalInfo;
    }

    public function setAdditionalInfo(?Info $additionalInfo): void
    {
        $this->additionalInfo = $additionalInfo;
    }
}
Copied!

1:n-relationship

A blog can have multiple posts in it. If a blog is deleted all of its posts should be deleted. However a blog might get displayed without displaying the posts therefore we load the posts of a blog lazily:

EXT:blog_example/Classes/Domain/Model/Blog.php
use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;
use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Blog extends AbstractEntity
{
    /**
     * The posts of this blog
     *
     * @var ObjectStorage<Post>
     * @Lazy
     * @Cascade("remove")
     */
    public $posts;

    /**
     * Adds a post to this blog
     */
    public function addPost(Post $post)
    {
        $this->posts->attach($post);
    }

    /**
     * Remove a post from this blog
     */
    public function removePost(Post $postToRemove)
    {
        $this->posts->detach($postToRemove);
    }

    /**
     * Returns all posts in this blog
     *
     * @return ObjectStorage
     */
    public function getPosts(): ObjectStorage
    {
        return $this->posts;
    }

    /**
     * @param ObjectStorage<Post> $posts
     */
    public function setPosts(ObjectStorage $posts): void
    {
        $this->posts = $posts;
    }
}
Copied!

Each post belongs to exactly one blog, of course a blog does not get deleted when one of its posts gets deleted.

EXT:blog_example/Classes/Domain/Model/Post.php
class Post extends AbstractEntity
{
    protected Blog $blog;
}
Copied!

A post can also have multiple comments and each comment belongs to exactly one blog. However we never display a comment without its post therefore we do not need to store information about the post in the comment's model: The relationship is unidirectional.

EXT:blog_example/Classes/Domain/Model/Post.php
use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;
use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Post extends AbstractEntity
{
    /**
     * @var ObjectStorage<Comment>
     * @Lazy
     * @Cascade("remove")
     */
    public ObjectStorage $comments;
}
Copied!

The model of the comment has no property to get the blog post in this case.

n:1-relationships

n:1-relationships are the same like 1:n-relationsships but from the perspective of the object:

Each post has exactly one main author but an author can write several blog posts or none at all. He can also be a second author and no main author.

EXT:blog_example/Classes/Domain/Model/Post.php
class Post extends AbstractEntity
{
    /**
     * @var Person
     */
    protected Person $author;

    protected Person|null $secondAuthor;
}
Copied!

Once more the model of the author does not have a property containing the authors posts. If you would want to get all posts of an author you would have to make a query in the PostRepository taking one or both relationships (first author, second author) into account.

m:n-relationship

A blog post can have multiple categories, each category can belong to multiple blog posts.

EXT:blog_example/Classes/Domain/Model/Post.php
class Post extends AbstractEntity
{
    /**
     * @var Person
     */
    protected ?Person $author = null;

    protected ?Person $secondAuthor = null;
}
Copied!

Event

The PSR-14 event AfterObjectThawedEvent is available to modify values when creating domain objects.

Eager loading and lazy loading

By default, Extbase loads all child objects with the parent object (so for example, all posts of a blog). This behavior is called eager loading. The annotation @TYPO3\CMS\Extbase\Annotation\ORM\Lazy causes Extbase to load and build the objects only when they are actually needed (lazy loading). This can lead to a significant increase in performance.

On cascade remove

The annotation @TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove") has the effect that, if a blog is deleted, its posts will also be deleted immediately. Extbase usually leaves all child objects' persistence unchanged.

Besides these two, there are a few more annotations available, which will be used in other contexts. For the complete list of all Extbase supported annotations, see the chapter Annotations.

Identifiers in localized models

Domain models have a main identifier uid and an additional property _localizedUid.

Depending on whether the languageOverlayMode mode is enabled (true or 'hideNonTranslated') or disabled (false), the identifier contains different values.

When languageOverlayMode is enabled, then the uid property contains the uid value of the default language record, the uid of the translated record is kept in the _localizedUid.

Context Record in language 0 Translated record
Database uid:2 uid:11, l10n_parent:2
Domain object values with languageOverlayMode enabled uid:2, _localizedUid:2 uid:2, _localizedUid:11
Domain object values with languageOverlayMode disabled uid:2, _localizedUid:2 uid:11, _localizedUid:11