---
title: "Extbase domain model"
manual: "TYPO3 Explained"
version: "main"
permalink: "https://docs.typo3.org/permalink/t3coreapi:extbase-model@main"
source: "ExtensionArchitecture/Extbase/Domain/Model.rst"
rendered: "2026-09-20T15:52:37+00:00"
---

# Extbase domain model {#extbase-model}

A domain model is a PHP class that represents one type of data that your extension
works with, for example, an event, a product, a blog post, or a speaker. Each instance of
the class corresponds to one database record. Extbase maps between the two
automatically.

**On this page**

-   [AbstractEntity — what you get for free](https://docs.typo3.org/permalink/t3coreapi:abstractentity-what-you-get-for-free@main)
-   [Defining model properties in Extbase](https://docs.typo3.org/permalink/t3coreapi:defining-model-properties-in-extbase@main)
-   [PHP attributes in Extbase domain models](https://docs.typo3.org/permalink/t3coreapi:php-attributes-in-extbase-domain-models@main)
-   [Modelling relations in Extbase](https://docs.typo3.org/permalink/t3coreapi:modelling-relations-in-extbase@main)
-   [File reference properties](https://docs.typo3.org/permalink/t3coreapi:file-reference-properties@main)
-   [Enum properties in Extbase domain models](https://docs.typo3.org/permalink/t3coreapi:enum-properties-in-extbase-domain-models@main)
-   [Non-persisted (transient) properties in Extbase models](https://docs.typo3.org/permalink/t3coreapi:non-persisted-transient-properties-in-extbase-models@main)
-   [Table and field mapping](https://docs.typo3.org/permalink/t3coreapi:table-and-field-mapping@main)
-   [Configuring persistence for Extbase models](https://docs.typo3.org/permalink/t3coreapi:configuring-persistence-for-extbase-models@main)
-   [Value objects in Extbase domain models](https://docs.typo3.org/permalink/t3coreapi:value-objects-in-extbase-domain-models@main)
-   [Hydrating / thawing objects of Extbase models](https://docs.typo3.org/permalink/t3coreapi:hydrating-thawing-objects-of-extbase-models@main)

## `AbstractEntity` — what you get for free {#extbase-domain-model-abstract-entity}

Every persisted domain object extends
`\TYPO3\CMS\Extbase\DomainObject\AbstractEntity`:

**EXT:my_extension/Classes/Domain/Model/Conference.php**

```php
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Conference extends AbstractEntity
{
    // your properties here
}
```

You do **not** need to declare `$uid` or `$pid` — they are inherited.
`AbstractEntity` provides:

-   `getUid(): ?int` — returns `null` until the object is persisted
-   `getPid(): ?int` — the page UID the record lives on
-   `setPid(int $pid): void`
-   Dirty-state tracking — Extbase knows which properties have changed since the
    object was loaded and only writes those columns on `update()`

> [!NOTE]
> Do not extend `AbstractDomainObject`.
> `AbstractEntity`
> is the correct base class for objects that have identity (a UID).
> `AbstractValueObject` exists but
> is marked `@internal` — see [Value objects in Extbase domain models](https://docs.typo3.org/permalink/t3coreapi:extbase-domain-model-value-objects@main) below.

## Defining model properties in Extbase {#extbase-model-properties-untyped}

Properties should be `protected`, typed, and have a default value:

**EXT:my_extension/Classes/Domain/Model/Conference.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\Attribute\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Conference extends AbstractEntity
{
  #[Validate(validator: 'NotEmpty')]
  protected string $title = '';

  protected string $description = '';

  protected ?\DateTimeImmutable $conferenceDate = null;

  protected bool $published = false;

  public function getTitle(): string
  {
    return $this->title;
  }

  public function setTitle(string $title): void
  {
    $this->title = $title;
  }

  public function getDescription(): string
  {
    return $this->description;
  }

  public function setDescription(string $description): void
  {
    $this->description = $description;
  }

  public function getEventDate(): ?\DateTimeImmutable
  {
    return $this->conferenceDate;
  }

  public function setEventDate(?\DateTimeImmutable $conferenceDate): void
  {
    $this->conferenceDate = $conferenceDate;
  }

  public function isPublished(): bool
  {
    return $this->published;
  }

  public function setPublished(bool $published): void
  {
    $this->published = $published;
  }
}

```

Key rules:

-   Declare properties as `protected`. Public properties work but bypass
    getters and setters, making lazy loading and dirty-state tracking harder to
    reason about. Private properties are never populated during
    hydration
    — PHP prevents the parent `_setProperty()` method from writing to them
    — and changes to private properties are never persisted for the same reason.
    See [Model properties declared private are never populated](https://docs.typo3.org/permalink/t3coreapi:extbase-appendix-pitfalls-private-properties@main).
-   Every property needs a meaningful default so that the object is always in a
    valid state before it is populated by Extbase or your code.
-   Provide getters. Provide setters for properties that should be changeable
    after construction. Read-only properties onyl need a getter.
-   Boolean properties follow the `is*()` / `set*()` convention
    (`isPublished()`, not `getPublished()`).

**Column mapping convention:** Extbase maps camelCase property names to
snake_case database columns automatically. The property `$conferenceDate`
maps to the column `conference_date`. If your table or column names do not
follow this convention, override the mapping in
[`Configuration/Extbase/Persistence/Classes.php`](../../FileStructure/Configuration/Extbase/Persistence/Index.md#file-extension-configuration-extbase-persistence-classes-php). See
[Table and field mapping](https://docs.typo3.org/permalink/t3coreapi:extbase-domain-model-mapping@main) for the full syntax.

> [!NOTE]
> **See also**
>
> [Private properties silently ignored](https://docs.typo3.org/permalink/t3coreapi:extbase-appendix-pitfalls-private-properties@main) for why
> private properties are silently ignored, with the full technical
> explanation.
>
> Field and table mapping overrides are covered in the mapping reference
> (coming soon) and in [storagePid — when findAll() returns nothing](https://docs.typo3.org/permalink/t3coreapi:extbase-domain-repository-storagepid@main).

## PHP attributes in Extbase domain models {#extbase-domain-model-attributes}

Extbase uses native PHP attribute syntax to control persistence behaviour and
validation.

<!-- TODO: no Markdown rendering for "versionchanged" -->

DocBlock annotation support was removed. See
Annotations replaced by PHP attributes (TYPO3 v12 / required from v14) for the migration table
and the relevant Rector rule.

The four possible attributes on model properties are:

-   **#\[Lazy\]**

    -   *Type:* `Lazy`

    Defers loading of a related object or `ObjectStorage` until the
    getter is first called. Use on relations in list views where you often
    do not need the related data.

-   **#\[Cascade('remove')\]**

    -   *Type:* `Cascade`

    Deletes related objects automatically when the owning object is
    deleted. Only `'remove'` is supported.

-   **#\[Transient\]**

    -   *Type:* `Transient`

    Excludes a property from persistence. Useful for computed
    values or temporary state that should never reach the database.

-   **#\[Validate\]**

    -   *Type:* `Validate`

    Declares a validation rule on a property. The validator runs when
    the object is submitted via a controller action.
    `#[Validate]` is repeatable — apply multiple validators to one
    property.

Import from the `\TYPO3\CMS\Extbase\Attribute\ORM` namespace:

**EXT:my_extension/Classes/Domain/Model/Conference.php**

```php
use TYPO3\CMS\Extbase\Attribute\ORM\Cascade;
use TYPO3\CMS\Extbase\Attribute\ORM\Lazy;
use TYPO3\CMS\Extbase\Attribute\ORM\Transient;
use TYPO3\CMS\Extbase\Attribute\Validate;

// on model properties:
#[Validate(validator: 'NotEmpty')]
protected string $title = '';

#[Validate(validator: 'StringLength', options: ['minimum' => 3, 'maximum' => 50])]
protected string $slug = '';

#[Lazy]
#[Cascade('remove')]
protected ObjectStorage $comments;

#[Transient]
protected ?string $computedLabel = null;
```

> [!WARNING]
> If you are migrating from an older extension, replace all DocBlock
> annotations (`@Extbase\ORM\Lazy`, `@Extbase\Validate`, etc.)
> with their attribute equivalents. The old syntax causes a silent failure
> in v14 meaning that Extbase will ignore the annotation without an error.

> [!NOTE]
> **See also**
>
> [Extbase PHP attributes](https://docs.typo3.org/permalink/t3coreapi:extbase-appendix-attributes@main) for all Extbase PHP attributes
> with parameters and usage examples

## Modelling relations in Extbase {#extbase-model-relations}

A relation is just a property, and Extbase offers two shapes for it: a property
that holds **one other object**, and a property that holds **many objects** in an
`ObjectStorage`. The following example shows both — a relation to one
`Location` and a relation to many `Comment` objects:

**EXT:my_extension/Classes/Domain/Model/Conference.php (with relations)**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\Attribute\ORM\Cascade;
use TYPO3\CMS\Extbase\Attribute\ORM\Lazy;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Conference extends AbstractEntity
{
  protected string $title = '';

  // a relation to one other object — a nullable typed property
  protected Location|LazyLoadingProxy|null $location = null;

  /**
   * @var ObjectStorage<Comment>
   */
  #[Lazy]
  #[Cascade('remove')]
  protected ObjectStorage $comments;

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

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

  public function getTitle(): string
  {
    return $this->title;
  }

  public function setTitle(string $title): void
  {
    $this->title = $title;
  }

  public function getLocation(): ?Location
  {
    // type check only for phpstan
    if ($this->location instanceof LazyLoadingProxy) {
      $this->location = $this->location->_loadRealInstance();
    }
    return $this->location;
  }

  public function setLocation(?Location $location): void
  {
    $this->location = $location;
  }

  public function getComments(): ObjectStorage
  {
    return $this->comments;
  }

  public function addComment(Comment $comment): void
  {
    $this->comments->attach($comment);
  }

  public function removeComment(Comment $comment): void
  {
    $this->comments->detach($comment);
  }
}

```

A few things to note in the example above:

-   **A relation to a single other object** is a single typed property, nullable when
    the related object is optional. If `#[Lazy]` is applied, Extbase
    installs a
    `LazyLoadingProxy` instead
    of loading the related object immediately. The union type
    `Location|LazyLoadingProxy|null` is required so that Extbase can set the
    proxy. The `instanceof LazyLoadingProxy` check in the getter exists
    solely for static analysis — without it PHPStan cannot narrow the return
    type to `?Location`. If you do not need a precisely typed getter, the
    proxy resolves automatically on any access and the check can be omitted.
-   **A relation to many objects** is an `ObjectStorage`. You read it by
    iterating, and change it through `addComment()` / `removeComment()`
    methods that call `attach()` and `detach()` — never manipulate the
    storage property directly. The `@var ObjectStorage<Comment>` annotation
    is required for IDE autocompletion and static analysis, even though PHP does
    not enforce the generic type.
-   **Each ObjectStorage must be initialised**, otherwise the first access
    to the typed property triggers a fatal error. Do this in
    `initializeObject()` and call that method from the constructor. Extbase
    calls `initializeObject()` itself after mapping a record from the
    database, so the storage is ready on loaded objects; calling it from
    `__construct()` as well covers objects you create with `new`. Both
    code paths then end up with an initialised storage.
-   `#[Lazy]` on an `ObjectStorage` means Extbase loads the related
    records only when you first iterate over the storage or call a method on it.
    This avoids loading potentially hundreds of related records just because the
    parent object is loaded.
-   `#[Cascade('remove')]` on `$comments` means that when this
    `Conference` object is deleted, all related `Comment` objects are also
    deleted. A comment has no life outside its event, so this is the right
    choice. Without this attribute, deleting the event would leave orphaned comment
    records in the database. Use cascade remove only when the related objects
    genuinely belong to the parent and have no independent existence.

> [!NOTE]
> **See also**
>
> -   [Object relations in Extbase](https://docs.typo3.org/permalink/t3coreapi:extbase-persistence-relations@main) — explains the two relation types, how they are stored, unidirectional and bidirectional relations, lazy loading, and the N+1 query problem.
> -   [Extbase PHP attributes](https://docs.typo3.org/permalink/t3coreapi:extbase-appendix-attributes@main) — all Extbase PHP attributes, with parameters and usage examples.

## File reference properties {#extbase-domain-model-filereference}

A model property that maps to a
FAL file uses
`FileReference` — Extbase's own
thin wrapper around the `sys_file_reference` table. A single file becomes
a nullable property. A collection uses
`ObjectStorage`:

**EXT:my_extension/Classes/Domain/Model/Conference.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Conference extends AbstractEntity
{
  protected ?FileReference $logo = null;

  /**
   * @var ObjectStorage<FileReference>
   */
  protected ObjectStorage $impressions;

  public function __construct()
  {
    $this->impressions = new ObjectStorage();
  }

  public function initializeObject(): void
  {
    $this->impressions = $this->impressions ?? new ObjectStorage();
  }

  public function getLogo(): ?FileReference
  {
    return $this->logo;
  }

  public function setLogo(?FileReference $logo): void
  {
    $this->logo = $logo;
  }

  /**
   * @return ObjectStorage<FileReference>
   */
  public function getImpressions(): ObjectStorage
  {
    return $this->impressions;
  }

  public function setImpressions(ObjectStorage $impressions): void
  {
    $this->impressions = $impressions;
  }
}

```

The corresponding TCA column must be of [type=file](https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ColumnsConfig/Type/File/Index.html#columns-file).

In a Fluid template, pass the
`FileReference` object to
[f:image](https://docs.typo3.org/other/typo3/view-helper-reference/main/en-us/Global/Image.html#typo3-fluid-image). This will honour crop
configuration or any additional properties set in the TYPO3 backend for that file reference:

**EXT:my_extension/Resources/Private/Templates/Conference/Show.fluid.html**

```html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
  data-namespace-typo3-fluid="true">

<f:layout name="Default" />

<f:section name="main">
  <f:if condition="{conference.logo}">
    <f:image image="{conference.logo}" cropVariant="default" alt="{conference.title}" />
  </f:if>

  <f:for each="{conference.impressions}" as="impression">
    <f:image image="{impression}" cropVariant="default" alt="{conference.title}" />
  </f:for>
</f:section>

</html>

```

> [!NOTE]
> **See also**
>
> [File uploads in Extbase domain models](https://docs.typo3.org/permalink/t3coreapi:extbase-domain-fileupload@main) for handling file uploads submitted through a
> frontend form, including the `#[FileUpload]` attribute, validation,
> and deletion.

## Enum properties in Extbase domain models {#extbase-domain-model-enums}

[Backed enums](https://www.php.net/manual/en/language.enumerations.backed.php)
— enums with an underlying `string` or `int` value (introduced in
PHP 8.1) — work in Extbase
models without any extra configuration. Extbase's built-in
`EnumConverter` converts the
stored value to and from the enum instance.

Define the enum as follows:

**EXT:my_extension/Classes/Domain/Model/Enum/Salutation.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model\Enum;

enum Salutation: string
{
  case None = '';
  case Mr = 'mr';
  case Ms = 'ms';
  case Mx = 'mx';
}

```

Use it as a model property:

**EXT:my_extension/Classes/Domain/Model/Speaker.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use MyVendor\MyExtension\Domain\Model\Enum\Salutation;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Speaker extends AbstractEntity
{
  protected string $name = '';

  protected Salutation $salutation = Salutation::None;

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

  public function setName(string $name): void
  {
    $this->name = $name;
  }

  public function getSalutation(): Salutation
  {
    return $this->salutation;
  }

  public function setSalutation(Salutation $salutation): void
  {
    $this->salutation = $salutation;
  }
}

```

The database column stores the raw backing value (`''`, `'mr'`,
`'ms'`, `'mx'`). Extbase converts it to the enum case on read and
back to the string on write.

> [!NOTE]
> Pure
> [unit enums](https://www.php.net/manual/en/language.enumerations.basics.php)
> (without a backing type) are not supported because there is no stable scalar
> value to store in the database. Always use backed enums for model
> properties.

## Non-persisted (transient) properties in Extbase models {#extbase-domain-model-transient}

Mark a property as `#[Transient]` to exclude it from persistence.
Extbase will never read or write the corresponding column. The property should then be
populated by your own code, which is typically a getter that computes a value using
other properties:

**EXT:my_extension/Classes/Domain/Model/Conference.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\Attribute\ORM\Transient;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Conference extends AbstractEntity
{
  protected string $title = '';

  protected ?\DateTimeImmutable $conferenceDate = null;

  #[Transient]
  protected ?string $displayLabel = null;

  public function getDisplayLabel(): string
  {
    if ($this->displayLabel === null) {
      $year = $this->conferenceDate?->format('Y');
      $this->displayLabel = $this->title . ' (' . $year . ')';
    }
    return $this->displayLabel;
  }
}

```

## Table and field mapping {#extbase-manual-mapping}

Extbase derives the database table name from the class name, for example, the class
`\MyVendor\MyExtension\Domain\Model\Conference` maps to the table
`tx_myextension_domain_model_conference`. Each
camelCase property maps to a snake_case column inside the table.

If a table or column does not match the convention, for example,
a table like `fe_users` which already exists in the system, override the mapping
in [`Configuration/Extbase/Persistence/Classes.php`](../../FileStructure/Configuration/Extbase/Persistence/Index.md#file-extension-configuration-extbase-persistence-classes-php):

**EXT:my_extension/Configuration/Extbase/Persistence/Classes.php**

```php
<?php

declare(strict_types=1);

return [
  \MyVendor\MyExtension\Domain\Model\FrontendUser::class => [
    'tableName' => 'fe_users',
    'properties' => [
      'firstName' => ['fieldName' => 'first_name'],
    ],
  ],
];

```

## Configuring persistence for Extbase models {#extbase-model-properties-default-values-tca}

A model class alone is not enough — TYPO3 also needs a
[TCA](https://docs.typo3.org/m/typo3/reference-tca/main/en-us/Index.html#start)
(Table Configuration Array) definition for the corresponding database table. TCA
tells TYPO3 which columns exist, what type they are, and how they behave in the backend. Without
TCA, neither the backend nor the database analyser would know anything about your
table.

The database analyser can create the actual database columns automatically from
TCA definitions. This means you **do not need**
[`ext_tables.sql`](../../FileStructure/ExtTablesSql.md#file-extension-ext-tables-sql) for standard column types — TYPO3 derives SQL from
the TCA and creates or updates the columns when the database analyser runs
(for example after installing or updating an extension).

You only need [`ext_tables.sql`](../../FileStructure/ExtTablesSql.md#file-extension-ext-tables-sql) for:

-   Custom column types not covered by TCA (for example `JSON`, spatial types)
-   Explicit indices beyond the defaults
-   Precise control over column length or collation

> [!TIP]
> Writing a model class and its TCA by hand is error-prone and
> repetitive. The [TYPO3 Kickstarter](https://packagist.org/packages/friendsoftypo3/kickstarter)
> ([`friendsoftypo3/kickstarter`](https://packagist.org/packages/friendsoftypo3/kickstarter)) can generate both together via
> `vendor/bin/typo3 make:*` commands. It is the recommended
> starting point when creating new models from scratch.

> [!NOTE]
> **See also**
>
> [Extension folder Configuration/TCA](https://docs.typo3.org/permalink/t3coreapi:extension-configuration-tca@main) — the `Configuration/TCA/`
> folder in your extension, where TCA files live.
>
> [TCA reference — column types](https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ColumnsConfig/Index.html)
> — full list of column types and their auto-creation support.
>
> [File structure](https://docs.typo3.org/permalink/t3coreapi:extension-files-locations@main) — complete extension file and folder
> structure reference.

## Value objects in Extbase domain models {#extbase-domain-model-value-objects}

In
[Domain-Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design),
a
[value object](https://en.wikipedia.org/wiki/Value_object)
is an object defined entirely by its value rather than by an identity. Two
value objects are equal if all their properties are equal — there is no UID,
no database row, no concept of "the same object over time". Classic examples are:
a monetary amount, a date range, a GPS coordinate, a color.

Value objects have three characteristics that make them useful:

-   **Immutable** — once created, they cannot be changed. Operations return a
    new instance rather than modifying an existing one.
-   **Equality by value** — two instances with identical properties are
    interchangeable. Compare them using an `equals()` method, not
    `===`.
-   **Self-validating** — the constructor rejects invalid state, so a value
    object that exists is always valid.

A `Color` value object is a straightforward example: `new Color('Midnight Blue', '#191970')`
and another `new Color('Midnight Blue', '#191970')` are equal and
interchangeable. Neither has an identity. You never update a colour, you
just replace it with a new one.

**In TYPO3 and Extbase**, value objects are implemented as plain PHP classes.
`\TYPO3\CMS\Extbase\DomainObject\AbstractValueObject` exists in v14 but
is marked `@internal`. It is not public API and must not be extended in
extension code.

**EXT:my_extension/Classes/Domain/Model/Color.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

final class Color
{
  public function __construct(
    public readonly string $name,
    public readonly string $hex,
  ) {
    if (!preg_match('/^#[0-9a-fA-F]{6}$/', $hex)) {
      throw new \InvalidArgumentException('Invalid hex color: ' . $hex);
    }
  }

  public function equals(self $other): bool
  {
    return $this->hex === $other->hex;
  }

  public function withName(string $name): self
  {
    return new self($name, $this->hex);
  }
}

```

Note that `withName()` returns a new `Color` instance rather than
modifying `$this` — that is the immutability principle in practice. The
constructor validates the hex format immediately, so an invalid `Color`
can never exist.

**Persistence:** value objects are not persisted as their own database records.
Store them as scalar columns on the owning entity and reconstruct the object in
a getter:

**EXT:my_extension/Classes/Domain/Model/Product.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Product extends AbstractEntity
{
  protected string $colorName = '';
  protected string $colorHex = '#000000';

  public function getColor(): Color
  {
    return new Color($this->colorName, $this->colorHex);
  }

  public function setColor(Color $color): void
  {
    $this->colorName = $color->name;
    $this->colorHex = $color->hex;
  }
}

```

If the value object genuinely needs its own table and identity, it is no longer
a value object — use `AbstractEntity` instead.

> [!NOTE]
> If the set of possible values is fixed and known at compile time — a
> salutation, a status, a priority level — use a backed enum instead.
> Enums are simpler, Extbase maps them automatically, and PHP enforces
> the construction of only valid cases. Value objects are the right
> choice when the value has structure, behaviour, or validation logic
> beyond what an enum can express.

> [!NOTE]
> **See also**
>
> [Enum properties in Extbase domain models](https://docs.typo3.org/permalink/t3coreapi:extbase-domain-model-enums@main) for backed
> enums as model properties, including automatic conversion by Extbase.

## Hydrating / thawing objects of Extbase models {#extbase-model-hydrating}

> [!WARNING]
> The information in this section might be outdated!

Hydrating (the term originates from [doctrine/orm](https://github.com/doctrine/orm)), or in Extbase terms thawing,
is the act of creating an object from a given database row. The responsible
class involved is the `\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper`.
During the process of hydrating, the `DataMapper` creates objects to map
the raw database data onto.

Before diving into the framework internals, let us take a look at models from
the user's perspective.

### Creating model objects with constructor arguments {#extbase-model-constructor}

Imagine you have a table `tx_extension_domain_codesnippets_blog` and a
corresponding model or entity (entity is used as a synonym here)
`\MyVendor\MyExtension\Domain\Model\Blog`.

Now, also imagine there is a domain rule which states, that all blogs must have
a title. This rule can easily be followed by letting the blog class have a
constructor with a required argument `string $title`.

**EXT:my_extension/Classes/Domain/Model/Blog.php**

```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(protected string $title)
  {
    // Property "posts" is not initialized on thawing / fetching from database!!
    // Must be initialized in initializeObject()!!
    $this->posts = new ObjectStorage();
  }
}

```

This example also shows how the `posts` property is initialized. It is done in
the constructor because PHP does not allow setting a default value that is of
type object.

### Hydrating objects with constructor arguments {#extbase-model-constructor-hydration}

Whenever the user creates new blog objects in extension code, the aforementioned
domain rule is followed. It is also possible to work on the `posts`
`ObjectStorage` without further initialization. `new Blog('title')` is
all one need to create a blog object with a valid state.

What happens in the `DataMapper` however, is a totally different thing.
When hydrating an object, the `DataMapper` cannot follow any domain rules.
Its only job is to map the raw database values onto a `Blog` instance. The
`DataMapper` could of course detect constructor arguments and try to guess
which argument corresponds to what property, but only if there is an easy
mapping, that means, if the constructor takes the argument `string $title`
and updates the property `title` with it.

To avoid possible errors due to guessing, the `DataMapper` simply ignores
the constructor at all. It does so with the help of the library
[doctrine/instantiator](https://github.com/doctrine/instantiator).

But there is more to all this.

### Initializing objects {#extbase-model-properties-default-values-initialize}

Have a look at the `$posts` property in the example above. If the
`DataMapper` ignores the constructor, that property is in an invalid state,
that means, uninitialized.

To address this problem and possible others, the `DataMapper` will call the
method `initializeObject(): void` on models, if it exists.

Here is an updated version of the model:

**EXT:my_extension/Classes/Domain/Model/Blog.php**

```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(protected string $title)
  {
    $this->initializeObject();
  }

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

```

This example demonstrates how Extbase expects the user to set up their models.
If the method `initializeObject()` is used for initialization logic that
needs to be triggered on initial creation **and** on hydration. Please mind
that `__construct()` **should** call `initializeObject()`.

If there are no domain rules to follow, the recommended way to set up a model
would then still be to define a `__construct()` and
`initializeObject()` method like this:

**EXT:my_extension/Classes/Domain/Model/Blog.php**

```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();
  }
}

```

> [!WARNING]
> Since Extbase does not call the constructor when thawing objects, special
> care must be taken regarding default values. See [Default values for model properties](https://docs.typo3.org/permalink/t3coreapi:extbase-domain-model-properties@main)
> for recommendations.

### Mutating objects {#extbase-model-mutation}

Some few more words on mutators (setter, adder, etc.). One might think that
`DataMapper` uses mutators during object hydration but it **does not**.
Mutators are the only way for the user (developer) to implement business rules
besides using the constructor.

The `DataMapper` uses the internal method
`AbstractDomainObject::_setProperty()` to update object properties. This
looks a bit dirty and is a way around all business rules but that is what the
`DataMapper` needs in order to leave the mutators to the users.

> [!WARNING]
> While the `DataMapper` does not use any mutators, other parts of
> Extbase do. Both, [validation](https://docs.typo3.org/permalink/t3coreapi:extbase-validation-overview@main) and property
> mapping, either use existing mutators or gather type information from them.

### Property visibility {#extbase-model-visibility}

One important thing to know is that Extbase needs entity properties to be
protected or public. As written in the former paragraph,
`AbstractDomainObject::_setProperty()` is used to bypass setters.
However, `AbstractDomainObject` is not able to access private properties of
child classes, hence the need to have protected or public properties.

### Dependency injection {#extbase-model-dependency-injection}

Without digging too deep into [dependency injection](https://docs.typo3.org/permalink/t3coreapi:dependencyinjection@main)
the following statements have to be made:

-   Extbase expects entities to be so-called prototypes, that means classes that
    do have a different state per instance.
-   `DataMapper` **does not** use dependency injection for the creation of
    entities, that means it does not query the object container. This also
    means, that dependency injection is not possible in entities.

If you think that your entities need to use/access services, you need to find
other ways to implement it.

### Using an event when a object is thawed {#extbase-model-event}

The PSR-14 event [AfterObjectThawedEvent](https://docs.typo3.org/permalink/t3coreapi:afterobjectthawedevent@main) is available to modify values
when creating domain objects.
