Persistence

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

Connecting the model to the database

The SQL structure for the database needs to be defined in the file EXT:{ext_key}/ext_tables.sql. An Extbase model requires a valid TCA for the table that should be used as a base for the model. Therefore you have to create a TCA definition in file EXT:ext_key/Configuration/TCA/tx_extkey_domain_model_mymodel.php.

It is recommended to stick to the following naming scheme for the table:

Recommended naming scheme for table names
tx_{extkey}_domain_model_{mymodel}

tx_blogexample_domain_model_info

The SQL table for the model can be defined like this:

EXT:blog_example/ext_tables.sql
CREATE TABLE tx_blogexample_domain_model_info (
   name varchar(255) DEFAULT '' NOT NULL,
   post int(11) DEFAULT '0' NOT NULL
);

The according TCA definition could look like that:

EXT:blog_example/Configuration/TCA/tx_blogexample_domain_model_info.php
<?php

return [
    'ctrl' => [
        'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_info',
        'label' => 'name',
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'versioningWS' => true,
        'transOrigPointerField' => 'l10n_parent',
        'transOrigDiffSourceField' => 'l10n_diffsource',
        'languageField' => 'sys_language_uid',
        'translationSource' => 'l10n_source',
        'origUid' => 't3_origuid',
        'delete' => 'deleted',
        'sortby' => 'sorting',
        'enablecolumns' => [
            'disabled' => 'hidden',
        ],
        'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_tag.gif',
    ],
    'columns' => [
        'name' => [
            'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_info.name',
            'config' => [
                'type' => 'input',
                'size' => 20,
                'eval' => 'trim',
                'required' => true,
                'max' => 256,
            ],
        ],
        'bodytext' => [
            'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_info.bodytext',
            'config' => [
                'type' => 'text',
                'enableRichtext' => true,
            ],
        ],
        'post' => [
            'config' => [
                'type' => 'passthrough',
            ],
        ],
        'sys_language_uid' => [
            'exclude' => true,
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
            'config' => [
                'type' => 'language',
            ],
        ],
        'l10n_parent' => [
            'displayCond' => 'FIELD:sys_language_uid:>:0',
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'items' => [
                    [
                        '',
                        0,
                    ],
                ],
                'foreign_table' => 'tx_blogexample_domain_model_info',
                'foreign_table_where' =>
                    'AND {#tx_blogexample_domain_model_info}.{#pid}=###CURRENT_PID###'
                    . ' AND {#tx_blogexample_domain_model_info}.{#sys_language_uid} IN (-1,0)',
                'default' => 0,
            ],
        ],
        'l10n_source' => [
            'config' => [
                'type' => 'passthrough',
            ],
        ],
        'l10n_diffsource' => [
            'config' => [
                'type' => 'passthrough',
                'default' => '',
            ],
        ],
        't3ver_label' => [
            'displayCond' => 'FIELD:t3ver_label:REQ:true',
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.versionLabel',
            'config' => [
                'type' => 'none',
            ],
        ],
        'hidden' => [
            'exclude' => true,
            'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.enabled',
            'config' => [
                'type' => 'check',
                'renderType' => 'checkboxToggle',
                'items' => [
                    [
                        0 => '',
                        1 => '',
                        'invertStateDisplay' => true,
                    ],
                ],
            ],
        ],
    ],
    'types' => [
        0 => ['showitem' => 'sys_language_uid, l10n_parent, hidden, name, bodytext'],
    ],
];

Use arbitrary database tables with an Extbase model

It is possible to use tables that do not convey to the naming scheme mentioned in the last section. In this case you have to define the connection between the database table and the file EXT:{ext_key}/Configuration/Extbase/Persistence/Classes.php.

In the following example, the table fe_users provided by the system extension frontend is used as persistence table for the model Administrator. Additionally the table fe_groups is used to persist the model FrontendUserGroup.

EXT:blog_example/Configuration/Extbase/Persistence/Classes.php
<?php

declare(strict_types=1);

return [
    \T3docs\BlogExample\Domain\Model\Administrator::class => [
        'tableName' => 'fe_users',
        'recordType' => \T3docs\BlogExample\Domain\Model\Administrator::class,
        'properties' => [
            'administratorName' => [
                'fieldName' => 'username',
            ],
        ],
    ],
    \T3docs\BlogExample\Domain\Model\FrontendUserGroup::class => [
        'tableName' => 'fe_groups',
    ],
];

The key recordType makes sure that the defined model is only used if the type of the record is set to \FriendsOfTYPO3\BlogExample\Domain\Model\Administrator. This way the class will only be used for administrators but not plain frontend users.

The array stored in properties to match properties to database field names if the names do not match.

Record types and persistence

It is possible to use different models for the same database table.

A common use case are related domain objects that share common features and should be handled by hierarchical model classes.

In this case the type of the model is stored in a field in the table, commonly in a field called record_type. This field is then registered as type field in the ctrl section of the TCA array:

EXT:my_extension/Configuration/TCA/tx_myextension_domain_model_something.php
return [
    'ctrl' => [
        'title' => 'Something',
        'label' => 'title',
        'type' => 'record_type',
        // …
    ],
];

The relationship between record type and preferred model is then configured in the Configuration/Extbase/Persistence/Classes.php file.

EXT:my_extension/Configuration/Extbase/Persistence/Classes.php
return [
    \MyVendor\MyExtension\Domain\Model\Something::class => [
        'tableName' => 'tx_myextension_domain_model_party',
        'recordType' => 'something',
        'subclasses' => [
            'oneSubClass' => \MyVendor\MyExtension\Domain\Model\SubClass1::class,
            'anotherSubClass' => MyVendor\MyExtension\Domain\Model\SubClass2::class,
        ],
    ],
];

It is then possible to have a general repository, SomethingRepository which returns both SubClass1 and SubClass2 objects depending on the value of the record_type field. This way related domain objects can as one in some contexts.